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

в 13:16, , рубрики: Consumer Driven Contracts, gitlab ci, pact, python, микросервисы, Тестирование веб-сервисов, Тестирование мобильных приложений

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

  • Краткое введение в 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
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

Источник


* - обязательные к заполнению поля