Это третий блог из серии публикаций о тестировании контрактов потребителей сервиса. Я представил концепцию в первом блоге. Второй блог посвящен написанию тестов с использованием 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
→ РЕГИСТРАЦИЯ