Цели данной публикации:
- Краткое введение в Consumer Driven Contracts (CDC)
- Настройка CI pipeline на основе CDC
Consumer Driven Contracts
В этой части мы пройдемся по основным моментам CDC. Данная статья не является исчерпывающей на тему контрактного тестирования. Существует достаточное количество материалов на эту тему на том же Хабре.
Для продолжения нам необходимо познакомиться с основными положениями CDC:
- Контактное тестирование находится на уровне Service/Integration Tests над Unit Tests согласно пирамиде автотестирования (Mike Cohn)
- Контрактное тестирование может применяться, когда есть 2 (или более) сервиса, которые взаимодействуют друг с другом
- Сonsumer driven подход означает, что первым шагом в реализации является написание теста на стороне потребителя. Результатом теста является пакт (контракт) в формате json, который описывает взаимодействие между потребителем (например, веб-интерфейс / мобильный интерфейс: сервис, который хочет получить некоторые данные) и поставщиком (например, серверный API: сервис, который предоставляет данные)
- Следующим шагом является проверка договора с провайдером. Это полностью осуществлено фреймворком Pact.
Итак, начнем с теста на стороне потребителя. Я использовал Pactman. Вот так выглядит тест:
import pytest
from pactman import Like
from model.client import Client
@pytest.fixture()
def consumer(pact):
return Client(pact.uri)
def test_app(pact, consumer):
expected = '123456789'
(pact
.given('provider in some state')
.upon_receiving("request to get user's phone number")
.with_request(
method='GET',
path=f'/phone/john',
)
.will_respond_with(200, body=Like(expected))
.given('provider in some state')
.upon_receiving("request to get non-existent user's phone number")
.with_request(
method='GET',
path=f'/phone/micky'
)
.will_respond_with(404)
)
with pact:
consumer.get_users_phone(user='john', host=pact.uri)
consumer.get_users_phone(user='micky', host=pact.uri)
Используя Pact DSL, мы описываем взаимодействия request/response. После запуска теста мы получаем новый файл ({consumer}-{provider}-pact.json):
{
"consumer": {
"name": 'basic_client'
},
"provider": {
"name": 'basic_flask_app'
},
"interactions": [
{
"providerStates": [
{
"name": "provider in some state",
"params": {}
}
],
"description": "request to get user's phone number",
"request": {
"method": "GET",
"path": "/phone/john"
},
"response": {
"status": 200,
"body": "123456789",
"matchingRules": {
"body": {
"$": {
"matchers": [
{
"match": "type"
}
]
}
}
}
}
},
{
"providerStates": [
{
"name": "provider in some state",
"params": {}
}
],
"description": "request to get non-existent user's phone number",
"request": {
"method": "GET",
"path": "/phone/micky"
},
"response": {
"status": 404
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
}
}
}
Далее, нам нужно передать пакт провайдеру для верификации. Это делается с помощью Pact Broker.
Pact Broker — это хранилище контрактов с некоторыми дополнительными функциями, которые позволяют нам отслеживать совместимость версий сервисов, а также генерировать network diagrams (взаимодействие сервисов).
Pact Broker
Пакт
Матрица версий
Проверка провайдера
Эта часть теста полностью выполнена силами фреймворка. После проверки результаты отправляются обратно в Pact Broker.
provider-verifier_1 | Verifying a pact between basic_client and basic_flask_app
provider-verifier_1 | Given provider in some state
provider-verifier_1 | request to get user's phone number
provider-verifier_1 | with GET /phone/john
provider-verifier_1 | returns a response which
provider-verifier_1 | WARN: Skipping set up for provider state 'provider in some state' for consumer 'basic_client' as there is no --provider-states-setup-url specified.
provider-verifier_1 | has status code 200
provider-verifier_1 | has a matching body
provider-verifier_1 | Given provider in some state
provider-verifier_1 | request to get non-existent user's phone number
provider-verifier_1 | with GET /phone/micky
provider-verifier_1 | returns a response which
provider-verifier_1 | WARN: Skipping set up for provider state 'provider in some state' for consumer 'basic_client' as there is no --provider-states-setup-url specified.
provider-verifier_1 | has status code 404
provider-verifier_1 |
provider-verifier_1 | 2 interactions, 0 failures
Запуск обеих частей теста в pipeline
Теперь, когда обе части контрактного тестирования разобраны, было бы неплохо запускать их при каждом коммите. Вот где Gitlab CI приходит на помощь. Pipeline jobs описаны в .gitlab-ci.yml
. Прежде чем мы перейдем к pipeline, мы должны сказать несколько слов о GitLab Runner, который является open-source проектом, и используется для запуска jobs и отправки результатов обратно в GitLab. Jobs могут выполняться локально или с использованием Docker-контейнеров. В нашем проекте мы используем Docker. Тестовая инфраструктура реализована в контейнерах и описана в docker-compose.yml
, находящимся в корне проекта.
version: '2'
services:
basic-flask-app:
image: registry.gitlab.com/tknino69/basic_flask_app:latest
ports:
- 5005:5005
postgres:
image: postgres
ports:
- 5432:5432
env_file:
- test-setup.env
volumes:
- db-data:/var/lib/postgresql/data/pgdata
pactbroker:
image: dius/pact-broker
links:
- postgres
ports:
- 80:80
env_file:
- test-setup.env
provider-states:
image: registry.gitlab.com/tknino69/cdc/provider-states:latest
build: provider-states
ports:
- 5000:5000
consumer-test:
image: registry.gitlab.com/tknino69/cdc/consumer-test:latest
command: ["sh", "-c", "find -name '*.pyc' -delete && pytest $${TEST}"]
links:
- pactbroker
environment:
- CONSUMER_VERSION=$CI_COMMIT_SHA
provider-verifier:
image: registry.gitlab.com/tknino69/cdc/provider-verifier:latest
build: provider-verifier
ports:
- 5001:5000
links:
- pactbroker
depends_on:
- consumer-test
- provider-states
command: ['sh', '-c', 'find -name "*.pyc" -delete
&& CONSUMER_VERSION=`curl --header "PRIVATE-TOKEN:$${API_TOKEN}"
https://gitlab.com/api/v4/projects/$${BASIC_CLIENT}/repository/commits | jq ".[0] .id" | sed -e "s/\x22//g"`
&& echo $${CONSUMER_VERSION}
&& pact-provider-verifier $${PACT_BROKER}/pacts/provider/$${PROVIDER}/consumer/$${CONSUMER}/version/$${CONSUMER_VERSION}
--provider-base-url=$${BASE_URL}
--pact-broker-base-url=$${PACT_BROKER}
--provider=$${PROVIDER}
--consumer-version-tag=$${CONSUMER_VERSION}
--provider-app-version=$${PROVIDER_VERSION} -v
--publish-verification-results=PUBLISH_VERIFICATION_RESULTS']
environment:
- PROVIDER_VERSION=$CI_COMMIT_SHA
- API_TOKEN=$API_TOKEN
env_file:
- test-setup.env
volumes:
db-data:
Итак, у нас есть сервисы, которые запускаются в контейнерах по мере необходимости.
Сервис провайдера:
basic-flask-app:
image: registry.gitlab.com/tknino69/basic_flask_app:latest
ports:
- 5005:5005
Pact Broker и его БД. Volumes позволяют нам иметь постоянное хранилище для пактов и результатов верификации провайдера:
postgres:
image: postgres
ports:
- 5432:5432
env_file:
- test-setup.env
volumes:
- db-data:/var/lib/postgresql/data/pgdata
pactbroker:
image: dius/pact-broker
links:
- postgres
ports:
- 80:80
env_file:
- test-setup.env
Сервис Provider States. На практике он должен приводить провайдер в определенное состояние (например, завести пользователя в базе данных). Однако в нашем примере он просто выполняет фиктивную функцию.
provider-states:
image: registry.gitlab.com/tknino69/cdc/provider-states:latest
build: provider-states
ports:
- 5000:5000
Сервис, который запускает Consumer Test. Обратите внимание на команду, которая запускается в контейнере find -name '* .pyc' -delete && pytest $$ {TEST}
consumer-test:
image: registry.gitlab.com/tknino69/cdc/consumer-test:latest
command: ["sh", "-c", "find -name '*.pyc' -delete && pytest $${TEST}"]
links:
- pactbroker
environment:
- CONSUMER_VERSION=$CI_COMMIT_SHA
Сервис Provider Verifier:
provider-verifier:
image: registry.gitlab.com/tknino69/cdc/provider-verifier:latest
build: provider-verifier
ports:
- 5001:5000
links:
- pactbroker
depends_on:
- consumer-test
- provider-states
command: ['sh', '-c', 'find -name "*.pyc" -delete
&& CONSUMER_VERSION=`curl --header "PRIVATE-TOKEN:$${API_TOKEN}"
https://gitlab.com/api/v4/projects/$${BASIC_CLIENT}/repository/commits | jq ".[0] .id" | sed -e "s/\x22//g"`
&& echo $${CONSUMER_VERSION}
&& pact-provider-verifier $${PACT_BROKER}/pacts/provider/$${PROVIDER}/consumer/$${CONSUMER}/version/$${CONSUMER_VERSION}
--provider-base-url=$${BASE_URL}
--pact-broker-base-url=$${PACT_BROKER}
--provider=$${PROVIDER}
--consumer-version-tag=$${CONSUMER_VERSION}
--provider-app-version=$${PROVIDER_VERSION} -v
--publish-verification-results=PUBLISH_VERIFICATION_RESULTS']
environment:
- PROVIDER_VERSION=$CI_COMMIT_SHA
- API_TOKEN=$API_TOKEN
env_file:
- test-setup.env
Consumer Pipeline
.gitlab-ci.yml
в корне проекта потребителя описывает процессы, которые выполняются на стороне потребителя:
image: gitlab/dind:latest
variables:
TEST: 'tests/docker-compose.app.yml'
CONSUMER_VERSION: $CI_COMMIT_SHA
BASIC_APP: '11993024'
services:
- gitlab/gitlab-runner:latest
before_script:
- docker login -u $GIT_USER -p $GIT_PASS registry.gitlab.com
stages:
- clone_test
- get_broker_up
- test
- verify_provider
- clean_up
clone test:
tags:
- cdc
stage: clone_test
script:
- git clone https://$GIT_USER:$GIT_PASS@gitlab.com/tknino69/cdc.git && ls -ali
artifacts:
paths:
- cdc/
broker:
tags:
- cdc
stage: get_broker_up
script:
- cd cdc && docker-compose -f docker-compose.yml up -d pactbroker
dependencies:
- clone test
test:
tags:
- cdc
stage: test
script:
- cd cdc && CONSUMER_VERSION=$CONSUMER_VERSION docker-compose -f docker-compose.yml -f $TEST up consumer-test
dependencies:
- clone test
provider verification:
tags:
- cdc
stage: verify_provider
script:
- curl -X POST -F token=$CI_JOB_TOKEN -F ref=master https://gitlab.com/api/v4/projects/$BASIC_APP/trigger/pipeline
when: on_success
clean up:
tags:
- cdc
stage: clean_up
script:
- cd cdc && docker-compose stop consumer-test
dependencies:
- clone test
Здесь происходит следующее:
В before_script
мы логинимся в наш реестр gitlab, используя переменные $GIT_USER и $ GIT_PASS, которые мы установили в разделе «Настройки»> «CI / CD»
- Далее, мы клонируем тестовый проект
- На следующем этапе мы поднимаем Pact Broker
- Затем запускается Consumer Test
- После этого используем Gitlab API для запуска верификации провайдера
- И, наконец, подчищаем за собой
Provider Pipeline
Конфигурация pipeline провайдера хранится в .gitlab-ci.yml
в корне проекта провайдера.
image: gitlab/dind:latest
variables:
TEST: 'tests/docker-compose.app.yml'
PROVIDER_VERSION: $CI_COMMIT_SHA
services:
- gitlab/gitlab-runner:latest
stages:
- clone_test
- provider_verification
- clean_up
clone test:
tags:
- cdc
stage: clone_test
script:
- git clone https://$GIT_USER:$GIT_PASS@gitlab.com/tknino69/cdc.git
artifacts:
paths:
- cdc/
verify provider:
tags:
- cdc
stage: provider_verification
before_script:
- cd cdc
- docker login -u $GIT_USER -p $GIT_PASS registry.gitlab.com && docker-compose -f docker-compose.yml up -d basic-flask-app
script:
- PROVIDER_VERSION=$PROVIDER_VERSION docker-compose -f docker-compose.yml -f $TEST up provider-verifier
dependencies:
- clone test
.clean up:
tags:
- cdc
stage: clean_up
script:
- cd cdc && docker-compose down --rmi local
Так же как и в Consumer Pipeline, у нас есть несколько jobs:
- Клонируем тестовый проект
- Верифицируем провайдера
- Подчищаем за собой
Суммируем:
- Написали контрактный тест на Python
- Настроили тестовую среду в Docker-контейнерах
- Настроили CI на основе контрактных тестов, т.е. commit в проект потребителя будет запускать CI pipeline(на стороне потребителя: клонирование тестовой среды -> запуск Pact Broker -> тестирование потребителя -> запуск верификации провайдера -> clean up; на стороне провайдера: клонирование тестовой среды -> верификация провайдера -> clean up).
Commit в проект провайдера инициирует верификацию провайдера для гарантирии соблюдения провайдером пакта
Спасибо за внимание.