
Это третий блог из серии публикаций о тестировании контрактов потребителей сервиса. Я представил концепцию в первом блоге. Второй блог посвящен написанию тестов с использованием Pact для синхронной коммуникации. В этом блоге мы рассмотрим, как писать тесты, когда среда коммуникации основана на сообщениях.
В нашем примере кредитный шлюз эмитирует событие о создании займа. Служба предоставления займов прослушивает его и выполняет дальнейшую обработку. В случае коммуникации на основе Http видно, что фреймворк Pact запускает имитатор Http-сервера. Коммуникация на основе сообщений отличается от Http тем, что не существует единого стандартного способа коммуникации. Она может быть организована с помощью различных инструментов, таких как Kafka, RabbitMQ, ActiveMQ и т.д. Pact может не связываться с этими инструментами, и, поэтому, он не запускает ни один из них во время выполнения тестов, а просто позволяет нам убедиться, что потребитель и производители событий придерживаются одной и той же схемы. В конечном итоге это то, что нам нужно! Давайте перейдем к коду.
Потребительский тест
Начнем с потребительского теста. В нашем примере листенер в службе предоставления займов является потребителем события, эмитируемого кредитным шлюзом. Ниже приведены шаги по созданию потребительских тестов и контракта.
1. Как обычно, начнем с теста spring boot. Поскольку класс LoanFulfilmentConsumer является потребителем в данном случае, мы напишем тест для него. На этот раз давайте сначала определим метод pact. Нам нужно создать MessagePact вместо RequestResponsePact. Ниже приведен метод.
@Pact(consumer = "loan_fulfilment_service", provider = "loan_gateway") fun eventForLoanFulfilment(builder: MessagePactBuilder): MessagePact { return builder .expectsToReceive("Loan creation event") .withMetadata(mapOf("traceId" to "1")) .withContent(PactDslJsonBody() .`object`("fraudCheck", PactDslJsonBody().booleanType("status")) .stringType("customerId")) .toPact() }
Метод не требует пояснений. Мы описываем в основном то, что содержит сообщение. Давайте разложим это по полочкам.
| Имена провайдера и потребителя, они будут опубликованы вместе с контрактом |
| Это описание взаимодействия, провайдер должен предоставить пример события из метода, аннотированного этим описанием |
| Это опционально и обозначает, будет ли сообщение содержать метаданные или нет. |
| Тело сообщения |
2. Предполагается, что тестовый метод принимает сообщения. И нам нужно убедиться, что оно соответствует объекту.
@Test fun `should return false fraud status`(messages: List<Message>) { messages.size shouldBe 1 jacksonObjectMapper().readValue( messages[0].contents.valueAsString(), LoanApplication::class.java) }
3. Необходимо явно указать, что провайдер будет асинхронным. Мы можем сделать это с помощью аннотации pactTestFor. Ниже приведен весь тест.
@ExtendWith(PactConsumerTestExt::class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @PactTestFor(providerType = ProviderType.ASYNCH) class LoanFulfilmentConsumerTest { @Pact(consumer = "loan_fulfilment_service", provider = "loan_gateway") fun eventForLoanFulfilment(builder: MessagePactBuilder): MessagePact { return builder .expectsToReceive("Loan creation event") .withMetadata(mapOf("traceId" to "1")) .withContent(PactDslJsonBody() .`object`("fraudCheck", PactDslJsonBody().booleanType("status")) .stringType("customerId")) .toPact() } @Test fun `should return false fraud status`(messages: List<Message>) { messages.size shouldBe 1 jacksonObjectMapper().readValue( messages[0].contents.valueAsString(), LoanApplication::class.java ) } }
Запуск этого теста создает контракт в папке target/pacts.
{ "consumer": { "name": "loan_fulfilment_service" }, "provider": { "name": "loan_gateway" }, "messages": [ { "description": "Loan creation event", "metaData": { "traceId": "1", "contentType": "application/json" }, "contents": { "customerId": "string", "fraudCheck": { "status": true } }, "matchingRules": { "body": { "$.fraudCheck.status": { "matchers": [ { "match": "type" } ], "combine": "AND" }, "$.customerId": { "matchers": [ { "match": "type" } ], "combine": "AND" } } } } ], "metadata": { "pactSpecification": { "version": "3.0.0" }, "pact-jvm": { "version": "4.0.10" } } }
Тест провайдера
1. На стороне провайдера нам нужно указать пример события, которое соответствует схеме, предоставленной потребителем.
2. Давайте начнем с теста spring boot и добавим тест pact, как показано ниже. Мы настраиваем контекст с AmpqTestTarget.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class LoanApplicationEventProviderTest { @BeforeEach fun setup(context: PactVerificationContext) { context.target = AmpqTestTarget() } @TestTemplate @ExtendWith(PactVerificationInvocationContextProvider::class) fun pactVerificationTestTemplate(context: PactVerificationContext) { context.verifyInteraction() } }
3. Фреймворк Pact не знает, какие взаимодействия тестировать из этих pact файлов. Давайте предоставим эту информацию с помощью аннотаций.
@PactFolder("target/pacts") @Provider("loan_gateway")
4. Запустите тест. Тест завершается с исключением No annotated methods were found for interaction 'Loan creation event'. You need to provide a method annotated with @PactVerifyProvider("Loan creation event") on the classpath that returns the message contents. (Не найдено аннотированных методов для взаимодействия 'Событие создания кредита'. Вам необходимо предоставить метод, аннотированный @PactVerifyProvider("Событие создания кредита") в пути класса, который возвращает содержимое сообщения.)
5. Взглянув на строку 'Loan creation event', мы понимаем, что упомянули ее в выражении expectsToReceive. Давайте вспомним нашу задачу. Провайдер должен предоставить пример события из метода, аннотированного этим описанием. Добавим метод для предоставления примера события.
@PactVerifyProvider("Loan creation event") fun exampleEvent(): MessageAndMetadata { val loanApplication = LoanApplication("1", FraudCheck(false)) val eventString = jacksonObjectMapper().writeValueAsString(loanApplication) return MessageAndMetadata(eventString.toByteArray(), mapOf("traceId" to "1")) }
Ниже приведен весь тест.
@PactFolder("target/pacts") @Provider("loan_gateway") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class LoanApplicationEventProviderTest { @BeforeEach fun setup(context: PactVerificationContext) { context.target = AmpqTestTarget() } @TestTemplate @ExtendWith(PactVerificationInvocationContextProvider::class) fun pactVerificationTestTemplate(context: PactVerificationContext) { context.verifyInteraction() } @PactVerifyProvider("Loan creation event") fun exampleEvent(): MessageAndMetadata { val loanApplication = LoanApplication("1", FraudCheck(false)) val eventString = jacksonObjectMapper().writeValueAsString(loanApplication) return MessageAndMetadata(eventString.toByteArray(), mapOf("traceId" to "1")) } }
6. Запустите тест. Теперь он пройдет и выдаст результат, как показано ниже.
Verifying a pact between loan_fulfilment_service and loan_gateway [Using Directory target/pacts] Loan creation event generates a message which has a matching body (OK) has matching metadata (OK)
Поздравляем! Теперь вы знаете, как написать контрактный тест для сервисов, взаимодействующих асинхронно. Весь код вы можете найти на github. В следующем блоге мы рассмотрим концепцию Pact broker.
Все коды на изображениях для копирования доступны здесь.
Материал подготовлен в рамках курса «Разработчик на Spring Framework».
Всех желающих приглашаем на двухдневный онлайн-интенсив «Работа с реляционными БД с помощью Spring». На двух занятиях вы узнаете, как работать с БД помощью разных технологий: JDBC, JPA + Hiberante, а также разберемся, какую помощь предлагают в этом различные проекты Spring - Spring JDBC, Spring ORM, Spring Data JPA и Spring Data JDBC
→ РЕГИСТРАЦИЯ
