Это третий блог из серии публикаций о тестировании контрактов  потребителей сервиса. Я представил концепцию в первом блоге. Второй блог посвящен написанию тестов с использованием 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()
}

Метод не требует пояснений. Мы описываем в основном то, что содержит сообщение. Давайте разложим это по полочкам.

@Pact(consumer ="loan_fulfilment_service", provider = "loan_gateway")

Имена провайдера и потребителя, они будут опубликованы вместе с контрактом

expectsToReceive("Loan creation event")

Это описание взаимодействия, провайдер должен предоставить пример события из метода, аннотированного этим описанием

withMetadata(mapOf("traceId" to "1"))

Это опционально и обозначает, будет ли сообщение содержать метаданные или нет.

withContent(...)

Тело сообщения

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

→ РЕГИСТРАЦИЯ

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