Цели данной публикации:
- Краткое введение в 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 в проект провайдера инициирует верификацию провайдера для гарантирии соблюдения провайдером пакта
Спасибо за внимание.
