Прошло много времени с тех пор, как мы говорили о тестировании и применении эффективных методов работы с TDD , особенно связанных с веб-сервисами REST (ful) и API. Но эта тема никогда не должна быть забыта, особенно в мире, где все делают микросервисы , что бы это ни означало, подразумевало или принимало.
Чтобы быть справедливым, существует довольно много областей, где архитектура на основе микросервиса сияет и позволяет организациям двигаться и внедрять инновации намного быстрее. Но без надлежащей дисциплины это также делает наши системы хрупкими, поскольку они становятся очень слабо связанными. В сегодняшнем посте мы поговорим о тестировании на основе контрактов и контрактах, ориентированных на потребителя, как о практических и надежных методах, гарантирующих выполнение нашими микросервисами своих обещаний.
Итак, как работает контрактное тестирование ? В двух словах, это удивительно простая техника, и она руководствуется следующими шагами:
- провайдер (скажем, Сервис A ) публикует свой контакт (или спецификацию), реализация может даже быть недоступна на данном этапе
- Потребитель (скажем, Услуга B ) следует этому контракту (или спецификации) для осуществления переговоров со Службой A
- Кроме того, потребитель вводит тестовый набор, чтобы проверить свои ожидания относительно выполнения контракта на обслуживание A
В случае веб-служб и API SOAP все очевидно, поскольку существует явный контракт в виде файла WSDL . Но в случае REST (FUL) API, есть много разных вариантов за углом ( WADL , RAML , Swagger , …) и до сих пор нет соглашения по одному. Это может показаться сложным, но, пожалуйста, не расстраивайтесь, потому что на помощь приходит Пакт !
Pact — это семейство платформ для поддержки тестирования контрактов, ориентированных на потребителя Существует множество языковых привязок и реализаций, включая JVM, JVM Pact и Scala-Pact . Чтобы развить такую экосистему полиглотов, Pact также включает специальную спецификацию, чтобы обеспечить взаимодействие между различными реализациями.
Отлично, Pact уже здесь, сцена готова, и мы готовы начать с некоторыми реальными фрагментами кода. Предположим, мы разрабатываем веб-API REST (ful) для управления людьми, используя потрясающие спецификации Apache CXF и JAX-RS 2.0 . Для простоты мы собираемся ввести только две конечные точки:
- POST / people / v1 для создания нового человека
- GET / people / v1? Email = <email>, чтобы найти человека по адресу электронной почты
По сути, мы можем не беспокоиться и просто сообщать эти минимальные части нашего контракта на обслуживание всем, поэтому потребители сами должны разобраться с этим (и действительно, Pact поддерживает такой сценарий). Но, конечно, мы не такие, мы заботимся и хотели бы подробно документировать наши API, вероятно, мы уже знакомы с Swagger . С этим вот наш PeopleRestService .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
@Api(value = "Manage people")@Path("/people/v1")@Consumes(MediaType.APPLICATION_JSON)@Produces(MediaType.APPLICATION_JSON)public class PeopleRestService { @GET @ApiOperation(value = "Find person by e-mail", notes = "Find person by e-mail", response = Person.class) @ApiResponses({ @ApiResponse(code = 404, message = "Person with such e-mail doesn't exists", response = GenericError.class) }) public Response findPerson( @ApiParam(value = "E-Mail address to lookup for", required = true) @QueryParam("email") final String email) { // implementation here } @POST @ApiOperation(value = "Create new person", notes = "Create new person", response = Person.class) @ApiResponses({ @ApiResponse(code = 201, message = "Person created successfully", response = Person.class), @ApiResponse(code = 409, message = "Person with such e-mail already exists", response = GenericError.class) }) public Response addPerson(@Context UriInfo uriInfo, @ApiParam(required = true) PersonUpdate person) { // implementation here }} |
Детали реализации на данный момент не важны, однако давайте взглянем на классы GenericError , PersonUpdate и Person, поскольку они являются неотъемлемой частью нашего сервисного контракта.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@ApiModel(description = "Generic error representation")public class GenericError { @ApiModelProperty(value = "Error message", required = true) private String message;}@ApiModel(description = "Person resource representation")public class PersonUpdate { @ApiModelProperty(value = "Person's first name", required = true) private String email; @ApiModelProperty(value = "Person's e-mail address", required = true) private String firstName; @ApiModelProperty(value = "Person's last name", required = true) private String lastName; @ApiModelProperty(value = "Person's age", required = true) private int age;}@ApiModel(description = "Person resource representation")public class Person extends PersonUpdate { @ApiModelProperty(value = "Person's identifier", required = true) private String id;} |
Превосходно! Как только у нас появятся аннотации Swagger и будет включена интеграция Apache CXF Swagger , мы сможем сгенерировать файл спецификации swagger.json , перенести его в пользовательский интерфейс Swagger и разослать каждому партнеру или заинтересованному потребителю.
Было бы здорово, если бы мы могли использовать эту спецификацию Swagger вместе с реализацией платформы Pact в качестве контракта на обслуживание. Благодаря Atlassian мы, безусловно, можем сделать это, используя swagger-request-validator , библиотеку для проверки HTTP- запроса / ответа на соответствие спецификации Swagger / OpenAPI, которая также хорошо интегрируется с Pact JVM .
Круто, теперь давайте перейдем от поставщика к потребителю и попытаемся выяснить, что мы можем сделать, имея такую спецификацию Swagger в наших руках. Оказывается, мы можем сделать много вещей. Например, давайте посмотрим на действие POST , которое создает нового человека. Как клиент (или потребитель) мы могли бы выразить наши ожидания в такой форме, что, имея действительную полезную нагрузку, отправленную вместе с запросом, мы ожидаем, что провайдер вернет код состояния HTTP 201, а полезная нагрузка ответа должна содержать нового человека с идентификатор назначен. На самом деле перевод этого утверждения в утверждения Pact JVM довольно прост.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Pact(provider = PROVIDER_ID, consumer = CONSUMER_ID)public PactFragment addPerson(PactDslWithProvider builder) { return builder .uponReceiving("POST new person") .method("POST") .path("/services/people/v1") .body( new PactDslJsonBody() .stringType("email") .stringType("firstName") .stringType("lastName") .numberType("age") ) .willRespondWith() .status(201) .matchHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .body( new PactDslJsonBody() .uuid("id") .stringType("email") .stringType("firstName") .stringType("lastName") .numberType("age") ) .toFragment();} |
Чтобы запустить процесс проверки контракта, мы собираемся использовать удивительный JUnit и очень популярную платформу REST Assured . Но перед этим давайте уточним, что такое PROVIDER_ID и CONSUMER_ID из приведенного выше фрагмента кода. Как и следовало ожидать, PROVIDER_ID является ссылкой на спецификацию контракта. Для простоты мы могли бы получить спецификацию Swagger из конечной точки PeopleRestService, и , к счастью, улучшения в тестировании Spring Boot делают эту задачу легкой и понятной .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = PeopleRestConfiguration.class)public class PeopleRestContractTest { private static final String PROVIDER_ID = "People Rest Service"; private static final String CONSUMER_ID = "People Rest Service Consumer"; private ValidatedPactProviderRule provider; @Value("${local.server.port}") private int port; @Rule public ValidatedPactProviderRule getValidatedPactProviderRule() { if (provider == null) { "/services/swagger.json", null, PROVIDER_ID, this); } return provider; }} |
CONSUMER_ID — это просто способ идентифицировать потребителя, не так много, чтобы сказать об этом. На этом мы готовы закончить наш первый тестовый пример:
|
1
2
3
4
5
6
7
8
|
@Test@PactVerification(value = PROVIDER_ID, fragment = "addPerson")public void testAddPerson() { given() .contentType(ContentType.JSON) .body(new PersonUpdate("tom@smith.com", "Tom", "Smith", 60)) .post(provider.getConfig().url() + "/services/people/v1");} |
Потрясающие! Как просто, просто обратите внимание на наличие аннотации @PactVerification, где мы ссылаемся на соответствующий проверочный фрагмент по имени, в этом случае он указывает на метод addPerson, который мы ввели ранее.
Отлично, но … какой смысл? Рад, что вы спрашиваете, потому что отныне любые изменения в контракте, которые могут быть несовместимыми с предыдущими версиями, будут нарушать наш тест. Например, если провайдер решит удалить свойство id из полезной нагрузки ответа, контрольный пример не пройден. Переименование свойств полезной нагрузки запроса, большое нет-нет, опять же, контрольный пример не пройдёт. Добавление новых параметров пути? Не повезло, контрольный пример не пропустит. Вы можете пойти еще дальше и потерпеть неудачу при каждом изменении контракта, даже если оно обратно совместимо (для точной настройки используется swagger-validator.properties ).
|
1
2
|
validation.response=ERRORvalidation.response.body.missing=ERROR |
Не очень хорошая идея, но все же, если она вам нужна, она есть. Точно так же, давайте добавим еще пару тестов для конечной точки GET , начиная с успешного сценария, где существует человек, которого мы ищем, например:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@Pact(provider = PROVIDER_ID, consumer = CONSUMER_ID)public PactFragment findPerson(PactDslWithProvider builder) { return builder .uponReceiving("GET find person") .method("GET") .path("/services/people/v1") .query("email=tom@smith.com") .willRespondWith() .status(200) .matchHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .body( new PactDslJsonBody() .uuid("id") .stringType("email") .stringType("firstName") .stringType("lastName") .numberType("age") ) .toFragment();}@Test@PactVerification(value = PROVIDER_ID, fragment = "findPerson")public void testFindPerson() { given() .contentType(ContentType.JSON) .queryParam("email", "tom@smith.com") .get(provider.getConfig().url() + "/services/people/v1");} |
Обратите внимание, что здесь мы ввели проверку строки запроса с помощью подтверждения запроса («email=tom@smith.com») . Следуя возможным результатам, давайте также рассмотрим неудачный сценарий, когда человек не существует, и мы ожидаем, что будет возвращена некоторая ошибка, наряду с кодом состояния 404 , например:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Pact(provider = PROVIDER_ID, consumer = CONSUMER_ID)public PactFragment findNonExistingPerson(PactDslWithProvider builder) { return builder .uponReceiving("GET find non-existing person") .method("GET") .path("/services/people/v1") .query("email=tom@smith.com") .willRespondWith() .status(404) .matchHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) .body(new PactDslJsonBody().stringType("message")) .toFragment();}@Test@PactVerification(value = PROVIDER_ID, fragment = "findNonExistingPerson")public void testFindPersonWhichDoesNotExist() { given() .contentType(ContentType.JSON) .queryParam("email", "tom@smith.com") .get(provider.getConfig().url() + "/services/people/v1");} |
Действительно блестящий, обслуживаемый, понятный и ненавязчивый подход для решения таких сложных и важных проблем, как тестирование на основе контракта и контракты, ориентированные на потребителя . Надеемся, что этот несколько новый метод тестирования поможет вам выявить больше проблем на этапе разработки, прежде чем они получат возможность проникнуть в производство.
Благодаря Swagger мы смогли сделать несколько ярлыков, но в случае, если у вас нет такой роскоши, у Pact достаточно богатая спецификация, которую вы всегда можете изучить и использовать. В любом случае, Pact JVM отлично справляется с задачей, помогая вам писать небольшие и сжатые тестовые примеры.
Полные источники проекта доступны на Github .
| Ссылка: | Выполняйте свои обещания: тестирование API JAX-RS на основе контракта от нашего партнера по JCG Андрея Редько в блоге Андрея Редько {devmind} . |

