Тестирование является неотъемлемой частью процесса разработки ПО. Согласно пирамиде тестирования Майка Коэна как не сказать про пирамиду тестирования можно выделить следующие виды тестирования:

  • модульное тестирование (60%)

  • интеграционное тестирование (20%)

  • приемочное тестирование (15%)

  • сквозное тестирование (5%)

где % — это соотношение количества тестов, которое может различаться от проекта к проекту.

Контрактное тестирование или Consumer Driven Contract (CDC) является связующим звеном между модульным и интеграционным тестированием. Тут важно отметить скорость выполнения тестов, а именно, чем выше по пирамиде находятся тесты, тем ниже скорость и выше сложность тестируемого взаимодействия или тем больше можно проверить интеграцию с другими ПО. Таким образом, при контрактном тестировании выше необходимость в поиске проблемных мест за один прогон теста. 

Итак, что же такое Consumer Driven Contract (CDC)?

Как известно, а также хорошо описано тут, каждый интерфейс имеет поставщика (supplier) и потребителя (consumer). Само собой, сервисы поставщика и потребителя распределены между разными командами, мы оказываемся в ситуации, когда чётко прописанный интерфейс между ними (или контракт) просто необходим. Обычно многие подходят к этой проблеме следующим образом:

  1. Пишут подробное описание спецификации интерфейса - контракт

  2. Реализуют сервис поставщика согласно спецификации

  3. Передают спецификацию интерфейса потребителю

  4. Ждут реализации от другой стороны

  5. Запускают ручные системные тесты, чтобы всё проверить

  6. Держат кулачки, что обе стороны будут вечно соблюдать описанный интерфейс

Сегодня многие компании заменили последние два шага на автоматизированные контрактные тесты, которые регулярно проверяют соответствие описания и реализации у поставщика и потребителя определённого контракта. Что является набором регрессионных тестов, которые обеспечивают раннее обнаружение отклонения от контракта.

Разберемся во взаимодействии на примере REST архитектуры: поставщик создает API c некоторым endpoint, а потребитель отправляет запрос к API, например, с целью получения данных или выполнения изменений в другом приложении.

Это контракт, который описывается с помощью DSL (domain-specific language). Он включает API описание в форме сценариев взаимодействия между потребителем и поставщиком. С помощью CDC выполняется тестирование клиента и API с использованием заглушек, которые собираются на основе контракта. Основной задачей CDC является сближение восприятия между командами разработчиков API и разработчиков клиента. Таким образом, участники команды потребителей пишут CDC тесты (для всех данных проекта разработки), чтобы команда поставщика смогла запустить тесты и проверить API. В итоге команда поставщика с легкостью разработает свой API, используя тесты CDC. Результатом прогона контрактных тестов является понимание, что поставщик уверен в исправной работе API у потребителя. 

Следует обратить внимание, что команда потребителя должна регулярно осуществлять поддержку CDC-тестов при каждом изменении, и вовремя передавать всю информацию команде поставщика. Если регулярно фиксируем неудачно выполненные CDC-тесты, то следует пойти (в буквальном смысле слова, к пострадавшей стороне теста и узнать, в рамках какой задачи были изменения (что привело к падению теста), а также уточнить, к чему в перспективе приведет данное изменение – прокачиваем софт скиллы:)). Из того, что было описано выше, можно выделить следующие тезисы для выполнения контрактного тестирования:

  1. Команда разработчиков (тестировщиков) со стороны потребителей пишет автоматизированные тесты с ожидаемыми параметрами со стороны потребителей.

  2. Тесты передаются команде поставщика.

  3. Команда поставщика запускает контрактные тесты и проверяет результат их выполнения. Если происходит падение тестов, то команды должны зафиксировать сбой и перепроверить документацию (согласованность разработки).

Использование CDC-тестов является важным шагом в разработке микросервисной архитектуры приложений, т.к. позволяет использовать автономные группы тестов. Т.к. в данный момент микросервисная архитектура становится широко применяемой в IT, то подход с CDC-тестированием становится популярным и, что немаловажно, достаточно эффективным из-за своей позиции в иерархии видов тестов.

Минусы CDC

Было сказано много чего хорошего, но какие минусы есть у CDC? В целом сводятся в основном к следующим ограничениям:

  1. CDC тесты не заменяют E2E тесты. По факту я склонен отнести CDC к заглушкам, которые являются моделями реальных компонентов, но не являются ими, т.е. это еще одна абстракция, которую нужно поддерживать и применять в нужных местах (сложно реализовать сложные сценарии).

  2. CDC тесты не заменяют функциональные тесты API. Лично придерживаюсь золотого правила – если убрать контракт и это не вызывает ошибки или неправильную работу клиента, то значит он не нужен. Пример: Нет необходимости проверять все коды ошибок через контракт, если клиент обрабатывает их (ошибки) одинаково. Таким образом контракт то, что важно для клиента сервиса, а не наоборот.

  3. CDC тесты дороже в поддержке, чем функциональные тесты.

  4. Для реализации CDC-тестов нужно использовать (изучать) отдельные инструменты тестирования – Spring Cloud Contract, PACT.

Инструменты для CDC 

Автор попробовал 2 тула Spring Cloud Contract и PACT. Protobuf можете изучить самостоятельно. Пришел к тому, что Spring Cloud Contract ему ближе Ниже в данной статье будет рассмотрен инструмент Spring Cloud Contract и будет приведен небольшой пример реализации контрактного теста. Если коротко об инструментах:

Spring Cloud Contract - Spring Cloud Contract is an umbrella project holding solutions that help users in successfully implementing the Consumer Driven Contracts approach. Currently Spring Cloud Contract consists of the Spring Cloud Contract Verifier project.

PACT - Pact is a code-first tool for testing HTTP and message integrations using contract tests. Contract tests assert that inter-application messages conform to a shared understanding that is documented in a contract. Without contract testing, the only way to ensure that applications will work correctly together is by using expensive and brittle integration tests.

Перейдем к практике и разберем стандартный пример использования контрактного тестирования с помощью Spring Cloud Contract.

Для начала нужно описать поставщика и потребителя услуг. Подготовьте стандартный проект с Java 1.8, maven 3.6.3, JUnit 4.12.

Проект будет иметь следующую структуру:

Поставщик

Добавьте зависимости org.springframework.cloud:

<dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-contract-verifier</artifactId>

    <version>2.1.1.RELEASE</version>

    <scope>test</scope>

</dependency>

Добавьте plugin:

<plugin>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-contract-maven-plugin</artifactId>

    <version>2.1.1.RELEASE</version>

    <extensions>true</extensions>

    <configuration>

        <baseClassForTests>

            com.baeldung.spring.cloud.springcloudcontractproducer.BaseTestClass

        </baseClassForTests>

    </configuration>

</plugin>

Сделаем RestController для поставщика с endpoint /validate/prime-number, на вход которого будем направлять число(“number”). На выходе мы получаем два значения – если на вход подается четное число вернется "Even", наоборот - "Odd". В коде это будет выглядеть следующим образом:

@RestController

public class EvenOddController {

   @GetMapping("/validate/prime-number")

   public String isNumberPrime(@RequestParam("number") Integer number) {

      return number % 2 == 0 ? "Even" : "Odd";

   }

}

Далее необходимо создать базовый класс для загрузки Spring context.

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)

@DirtiesContext

@AutoConfigureMessageVerifier

public class BaseTestClass {

   @Autowired

   private EvenOddController evenOddController;

   @Before

   public void setup() {

      StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(evenOddController);

      RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);

   }

}

В /src/test/resources/contracts/ добавим два контракта заглушки с расширением .groovy: 

Заглушка с проверкой четного числа

Contract.make {

    description "should return even when number input is even"

    request {

        method GET()

        url("/validate/prime-number") {

            queryParameters {

                parameter("number", "2")

            }

        }

    }

    response {

        body("Even")

        status 200

    }

}

Заглушка с проверкой нечетного числа

Contract.make {

    description "should return odd when number input is odd"

    request {

        method GET()

        url("/validate/prime-number") {

            queryParameters {

                parameter("number", "1")

            }

        }

    }

    response {

        body("Odd")

        status 200

    }

}

Примечание: при выполнении команды в инструменте сборки mvn clean install  выполняется генерация тестового класса на основе контракта с именем ContractVerifierTest, который наследует (расширяет) BaseTestClass в target. Методами сгенерированного класса являются контракты .groovy с приставкой validate_[имя контракта]. 

Пример: 

  • public void validate_shouldReturnEvenWhenRequestParamIsEven()

  • public void validate_shouldReturnOddWhenRequestParamIsOdd()

Результатом сборки также станет добавление заглушки в локальный репозиторий Maven, чтобы наши потребители могли ее использовать.

public class ContractVerifierTest extends BaseTestClass {

   @Test

   public void validate_shouldReturnEvenWhenRequestParamIsEven() throws Exception {

      // given:

         MockMvcRequestSpecification request = given();

      // when:

         ResponseOptions response = given().spec(request)

               .queryParam("number","2")

               .get("/validate/prime-number");

      // then:

         assertThat(response.statusCode()).isEqualTo(200);

      // and:

         String responseBody = response.getBody().asString();

         assertThat(responseBody).isEqualTo("Even");

   }

   @Test

   public void validate_shouldReturnOddWhenRequestParamIsOdd() throws Exception {

      // given:

         MockMvcRequestSpecification request = given();

      // when:

         ResponseOptions response = given().spec(request)

               .queryParam("number","1")

               .get("/validate/prime-number");

      // then:

         assertThat(response.statusCode()).isEqualTo(200);

      // and:

         String responseBody = response.getBody().asString();

         assertThat(responseBody).isEqualTo("Odd");

   }

}

Потребитель:

Потребители контрактного тестирования поддерживают контракт через заглушки, сгенерированные через http-соединение, таким образом изменение в поставщике нарушит контракт и приведет к ошибке.

Добавим зависимости в pom файл потребителя (consumer):

<dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-contract-wiremock</artifactId>

    <scope>test</scope>

</dependency>

<dependency>

    <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-contract-stub-runner</artifactId>

    <scope>test</scope>

</dependency>

<dependency>

    <groupId>com.peterwanghao.spring.cloud</groupId>

    <artifactId>spring-cloud-contract-producer</artifactId>

    <version>0.0.1-SNAPSHOT</version>

    <scope>test</scope>

</dependency>

Создадим RestController BasicMathController, который выполняет GET запрос для получения ответа от сгенерированной заглушки.

@RestController

public class BasicMathController {

   @Autowired

   private RestTemplate restTemplate;

   @GetMapping("/calculate")

   public String checkOddAndEven(@RequestParam("number") Integer number) {

      HttpHeaders httpHeaders = new HttpHeaders();

      httpHeaders.add("Content-Type", "application/json");

      ResponseEntity<String> responseEntity = restTemplate.exchange(

            "http://localhost:8090/validate/prime-number?number=" + number, HttpMethod.GET,

            new HttpEntity<>(httpHeaders), String.class);

      return responseEntity.getBody();

   }

}

Теперь пришло время настроить наш stub Runner, который сообщит нашему потребителю имеющихся заглушек, который находятся в локальном репозитории Maven от поставщика.

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)

@AutoConfigureMockMvc

@AutoConfigureJsonTesters

@AutoConfigureStubRunner(workOffline = true, ids = "com.peterwanghao.spring.cloud:spring-cloud-contract-producer:+:stubs:8090")

public class BasicMathControllerIntegrationTest {

   @Autowired

   private MockMvc mockMvc;

   @Test

   public void given_WhenPassEvenNumberInQueryParam_ThenReturnEven() throws Exception {

      mockMvc.perform(MockMvcRequestBuilders.get("/calculate?number=2").contentType(MediaType.APPLICATION_JSON))

            .andExpect(status().isOk()).andExpect(content().string("Even"));

   }

   @Test

   public void given_WhenPassOddNumberInQueryParam_ThenReturnOdd() throws Exception {

      mockMvc.perform(MockMvcRequestBuilders.get("/calculate?number=1").contentType(MediaType.APPLICATION_JSON))

            .andExpect(status().isOk()).andExpect(content().string("Odd"));

   }

}

Для запуска тестов выполните команду mvn clean install и проверьте результат.

Пример из кода можно найти на github.

Другие примеры можно посмотреть:

  1. Одно из первых знакомств с Spring Cloud Contract-Introduction

  2. Практика тестирования контрактов Spring Cloud Contract

  3. Spring-Cloud-Contract реальный бой

  4. Playing with Spring Cloud Contract

  5. Pact: best practices

  6. Martin Fowler: Contract Test 

  7. InfoQ: Creating Spring Cloud Contract Testing

  8. Pact with JS

  9. Protocol Buffers

  10. Spring Cloud Contract

  11. Spring Cloud Contract Samples

Комментарии (0)