- PVSM.RU - https://www.pvsm.ru -

Consumer Driven Contracts или Gitlab CI глазами QA test automation

Цели данной публикации:

  • Краткое введение в Consumer Driven Contracts (CDC)
  • Настройка CI pipeline на основе CDC

Consumer Driven Contracts

В этой части мы пройдемся по основным моментам CDC. Данная статья не является исчерпывающей на тему контрактного тестирования. Существует достаточное количество материалов на эту тему на том же Хабре [1].

Для продолжения нам необходимо познакомиться с основными положениями CDC:

  • Контактное тестирование находится на уровне Service/Integration Tests над Unit Tests согласно пирамиде автотестирования [2] (Mike Cohn)
  • Контрактное тестирование может применяться, когда есть 2 (или более) сервиса, которые взаимодействуют друг с другом
  • Сonsumer driven подход означает, что первым шагом в реализации является написание теста на стороне потребителя. Результатом теста является пакт (контракт) в формате json, который описывает взаимодействие между потребителем (например, веб-интерфейс / мобильный интерфейс: сервис, который хочет получить некоторые данные) и поставщиком (например, серверный API: сервис, который предоставляет данные)
  • Следующим шагом является проверка договора с провайдером. Это полностью осуществлено фреймворком Pact [3].

Итак, начнем с теста на стороне потребителя [4]. Я использовал Pactman [5]. Вот так выглядит тест:

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
image

Пакт
image

Матрица версий
image

Проверка провайдера

Эта часть теста полностью выполнена силами фреймворка. После проверки результаты отправляются обратно в 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»
image

  • Далее, мы клонируем тестовый проект
  • На следующем этапе мы поднимаем 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 в проект провайдера инициирует верификацию провайдера для гарантирии соблюдения провайдером пакта

Спасибо за внимание.

Автор: tknino

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/317144

Ссылки в тексте:

[1] Хабре: https://habr.com/ru/search/?q=consumer+driven+contracts#h

[2] пирамиде автотестирования: https://martinfowler.com/articles/practical-test-pyramid.html

[3] Pact: https://docs.pact.io/

[4] теста на стороне потребителя: https://docs.pact.io/how_pact_works#consumer-testing

[5] Pactman: https://github.com/reecetech/pactman

[6] Источник: https://habr.com/ru/post/451132/?utm_source=habrahabr&utm_medium=rss&utm_campaign=451132