Контракты, основанные на потребителях, с Пактом

Контракты по договорам, заключенным с потребителями

1. обзор

В этой быстрой статье мы рассмотрим концепцию контрактов, ориентированных на потребителя.

Мы будем тестировать интеграцию с внешней службой REST через контракт, который мы определяем с помощью библиотекиPact. That contract can be defined by the client, then picked up by the provider and used for development of its services.

Мы также создадим тесты на основе контракта для приложений клиента и провайдера.

2. Что такоеPact?

Using Pact, we can define consumer expectations for a given provider (that can be an HTTP REST service) in the form of a contract (отсюда и название библиотеки).

Мы собираемся заключить этот контракт, используя DSL, предоставленныйPact. После определения мы можем протестировать взаимодействие между потребителями и провайдером, используя фиктивный сервис, который создается на основе определенного контракта. Кроме того, мы протестируем сервис на соответствие контракту, используя фиктивный клиент.

3. Maven Dependency

Для начала нам нужно добавить зависимость Maven в библиотекуpact-jvm-consumer-junit_2.11:


    au.com.dius
    pact-jvm-consumer-junit_2.11
    3.5.0
    test

4. Определение контракта

Когда мы хотим создать тест с использованиемPact, сначала нам нужно определить@Rule, который будет использоваться в нашем тесте:

@Rule
public PactProviderRuleMk2 mockProvider
  = new PactProviderRuleMk2("test_provider", "localhost", 8080, this);

Мы передаем имя провайдера, хост и порт, на котором будет запущен макет сервера (который создается из контракта).

Допустим, служба определила контракт для двух HTTP-методов, которые она может обрабатывать.

Первый метод - это запрос GET, который возвращает JSON с двумя полями. Когда запрос завершается успешно, он возвращает код ответа HTTP 200 и заголовок Content-Type для JSON.

Давайте определим такой контракт с помощьюPact.

We need to use the @Pact annotation and pass the consumer name for which the contract is defined. Внутри аннотированного метода мы можем определить наш GET-контракт:

@Pact(consumer = "test_consumer")
public RequestResponsePact createPact(PactDslWithProvider builder) {
    Map headers = new HashMap<>();
    headers.put("Content-Type", "application/json");

    return builder
      .given("test GET")
        .uponReceiving("GET REQUEST")
        .path("/pact")
        .method("GET")
      .willRespondWith()
        .status(200)
        .headers(headers)
        .body("{\"condition\": true, \"name\": \"tom\"}")
        (...)
}

Используя DSLPact, мы определяем, что для данного запроса GET мы хотим вернуть ответ 200 с определенными заголовками и телом.

Вторая часть нашего контракта - это метод POST. Когда клиент отправляет запрос POST на путь/pact с правильным телом JSON, он возвращает код ответа HTTP 201.

Определим такой контракт сPact:

(...)
.given("test POST")
.uponReceiving("POST REQUEST")
  .method("POST")
  .headers(headers)
  .body("{\"name\": \"Michael\"}")
  .path("/pact")
.willRespondWith()
  .status(201)
.toPact();

Обратите внимание, что нам нужно вызвать методtoPact() в конце контракта, чтобы вернуть экземплярRequestResponsePact.

4.1. Результирующий артефакт Пакта

По умолчанию файлы Pact создаются в папкеtarget/pacts. Чтобы настроить этот путь, мы можем настроитьmaven-surefire-plugin:


    org.apache.maven.plugins
    maven-surefire-plugin
    
        
            target/mypacts
        
    
    ...

Сборка Maven сгенерирует файл с именемtest_consumer-test_provider.json в папкеtarget/mypacts, который содержит структуру запросов и ответов:

{
    "provider": {
        "name": "test_provider"
    },
    "consumer": {
        "name": "test_consumer"
    },
    "interactions": [
        {
            "description": "GET REQUEST",
            "request": {
                "method": "GET",
                "path": "/"
            },
            "response": {
                "status": 200,
                "headers": {
                    "Content-Type": "application/json"
                },
                "body": {
                    "condition": true,
                    "name": "tom"
                }
            },
            "providerStates": [
                {
                    "name": "test GET"
                }
            ]
        },
        {
            "description": "POST REQUEST",
            ...
        }
    ],
    "metadata": {
        "pact-specification": {
            "version": "3.0.0"
        },
        "pact-jvm": {
            "version": "3.5.0"
        }
    }
}

5. Тестирование клиента и провайдера с помощью контракта

Теперь, когда у нас есть контракт, мы можем использовать его для тестирования как для клиента, так и для поставщика.

Каждый из этих тестов будет использовать макет своего аналога, который основан на контракте, что означает:

  • клиент будет использовать фиктивный провайдер

  • провайдер будет использовать фиктивный клиент

Фактически, испытания проводятся в соответствии с контрактом.

5.1. Тестирование клиента

После того, как мы определили контракт, мы можем проверить взаимодействие со службой, которая будет создана на основе этого контракта. We can create normal JUnit test but we need to remember to put the @PactVerification annotation at the beginning of the test.с

Напишем тест для GET-запроса:

@Test
@PactVerification()
public void givenGet_whenSendRequest_shouldReturn200WithProperHeaderAndBody() {

    // when
    ResponseEntity response = new RestTemplate()
      .getForEntity(mockProvider.getUrl() + "/pact", String.class);

    // then
    assertThat(response.getStatusCode().value()).isEqualTo(200);
    assertThat(response.getHeaders().get("Content-Type").contains("application/json")).isTrue();
    assertThat(response.getBody()).contains("condition", "true", "name", "tom");
}

The @PactVerification annotation takes care of starting the HTTP service. В тесте нам нужно только отправить запрос GET и подтвердить, что наш ответ соответствует контракту.

Давайте также добавим тест для вызова метода POST:

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
String jsonBody = "{\"name\": \"Michael\"}";

// when
ResponseEntity postResponse = new RestTemplate()
  .exchange(
    mockProvider.getUrl() + "/create",
    HttpMethod.POST,
    new HttpEntity<>(jsonBody, httpHeaders),
    String.class
);

//then
assertThat(postResponse.getStatusCode().value()).isEqualTo(201);

Как мы видим, код ответа на запрос POST равен 201 - точно так же, как он был определен в контрактеPact.

Поскольку мы использовали аннотацию@PactVerification(), библиотекаPact запускает веб-сервер на основе ранее определенного контракта перед нашим тестовым примером.

5.2. Тестирование провайдера

Вторым этапом проверки нашего контракта является создание теста для провайдера с использованием фиктивного клиента на основе контракта.

Реализация нашего провайдера будет зависеть от этого контракта в стиле TDD.

В нашем примере мы будем использовать Spring Boot REST API.

Во-первых, чтобы создать наш тест JUnit, нам нужно добавить зависимостьpact-jvm-provider-junit_2.11:


    au.com.dius
    pact-jvm-provider-junit_2.11
    3.5.0
    test

Это позволяет нам создать тест JUnit, используяPactRunner и указав имя провайдера и расположение артефакта Пакта:

@RunWith(PactRunner.class)
@Provider("test_provider")
@PactFolder("pacts")
public class PactProviderTest {
    //...
}

Чтобы эта конфигурация работала, мы должны поместить файлtest_consumer-test_provider.json в папкуpacts нашего проекта службы REST.

Затем мы определим цель, которая будет использоваться для проверки взаимодействий в контракте, и запустим приложение Spring Boot перед запуском тестов:

@TestTarget
public final Target target = new HttpTarget("http", "localhost", 8082, "/spring-rest");

private static ConfigurableWebApplicationContext application;

@BeforeClass
public static void start() {
    application = (ConfigurableWebApplicationContext)
      SpringApplication.run(MainApplication.class);
}

Наконец, мы укажем состояния в контракте, которые мы хотим протестировать:

@State("test GET")
public void toGetState() { }

@State("test POST")
public void toPostState() { }

Запуск этого класса JUnit выполнит два теста для двух запросов GET и POST. Взглянем на журнал:

Verifying a pact between test_consumer and test_provider
  Given test GET
  GET REQUEST
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json" (OK)
      has a matching body (OK)

Verifying a pact between test_consumer and test_provider
  Given test POST
  POST REQUEST
    returns a response which
      has status code 201 (OK)
      has a matching body (OK)

Обратите внимание, что мы не включили здесь код для создания службы REST. Полный сервис и тест можно найти вGitHub project.

6. Заключение

В этом быстром уроке мы взглянули на Consumer Driven Contracts.

Мы создали контракт с помощью библиотекиPact. После того, как мы определили контракт, мы смогли проверить клиента и сервис на соответствие контракту и заявить, что они соответствуют спецификации.

Реализация всех этих примеров и фрагментов кода можно найти вGitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.