Message Pact with JUnit 5

Bhavesh Suvalaka
TechVerito
Published in
3 min readOct 30, 2021

In the world of Microservices, it’s essential to make sure that a change in one service doesn’t break anything in other services, keeping contracts in sync is important, and contract testing is a way to achieve it. There are a couple of ways to test the contract. One is to use a postman to write a contract test. One of the best options I come across is using Pact.

Pact provides a better way to test the contract between various consumers and producers. Let’s understand this using this example.

Let’s consider that we want to add a contract test between Order service and Shipment service. Whenever a user places an order, order service will publish OrderReceivedEvent and Shipment service needs to listen to this event, in order to start the shipment of an order.

This is the basic workflow, here we’ll first add the consumer contract test for shipment service. Let’s say below is the message that the Shipment Service is listening to.

data class OrderReceivedEvent(
val orderUuid: UUID,
val products: List<UUID>,
val paidAmount: Double,
val purchasedBy: String,
val purchasedDate: LocalDate
)

We want to make sure, order service is publishing the correct message as above, so we’ll add Consumer like so

@ExtendWith(PactConsumerTestExt::class)
@PactTestFor(providerName = "order-service", port = "8080", providerType = ProviderType.ASYNCH)
class PactConsumer {

@Pact(consumer = "shipment-service")
fun validOrderReceivedEvent(builder: MessagePactBuilder): MessagePact {
val body = expectedBodyForOrderReceivedEvent()
return builder.expectsToReceive("OrderReceivedEvent").withContent(body).toPact()
}

@Test
@PactTestFor(pactMethod = "validOrderReceivedEvent")
fun `test validOrderReceivedEvent`(messages: List<Message>) {
Assertions.assertNotEquals(messages.size, 0)
validateMessage(messages[0], OrderReceivedEvent::class.java)
}

private fun <T> validateMessage(message: Message, t: Class<T>) {
val mapper = ObjectMapper().registerKotlinModule()
val messageObj = mapper.readValue(String(message.contentsAsBytes()), t)
Assertions.assertNotNull(messageObj)
}

private fun expectedBodyForOrderReceivedEvent(): DslPart =
newJsonObject {
this.uuid("orderUuid")
.array("products", Consumer { it.uuid() })
.numberType("paidAmount")
.stringType("purchasedBy")
.datetime("purchasedDate")
}
}

Here PactConsumerTestExt is used to run the pact consumer test, the method annotated with@Pact mentions the consumer service name, this method returns the pact that needs to be validated. Also the @Test case has been added to make sure that the pact message that we want to validate contains a valid contract.

Once you run this pact consumer test, it will create the consumer contract at @Target/pact folder. You can configure your pipeline to publish this PactConsumer contract to Pact Broker.

Once the contract is published to Pact Broker, it’s essential to check the changes on the consumer side will not fail if it’s been merged to the master branch and to check that you can add the can-i-deploy check to your pipeline.

Now let’s look at the provider side of this.

Provider side, we want to verify that OrderReceivedEvent is compliant with the Shipment Service. Any changes in the contract doesn’t break anything at the consumer side. To verify that, we can add the PactVerify test, like so:

@Provider("order-service")
//@PactBroker(url = "brokerUrl")
@PactFolder("src/test/resources")
class PactProvider {

@TestTarget
private val target = MessageTestTarget()

@BeforeEach
fun before(context: PactVerificationContext) {
context.target = target
}

@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider::class)
fun pactVerificationTestTemplate(context: PactVerificationContext) {
context.verifyInteraction()
}

@PactVerifyProvider("OrderReceivedEvent")
fun verifyOrderReceivedEvent(): String? {
val df = SimpleDateFormat("yyyy-MM-dd")
val objectMapper = ObjectMapper().findAndRegisterModules()
objectMapper.dateFormat = df
return objectMapper.writeValueAsString(anOrderReceivedEvent)
}
}

Here I have kept the pact contract which consumer has generated at test/resources folder to test the pact between two parties, but if you are using the pact broker, (and that’s what should be used), you can mention the URL of the pact broker and remove the @PactFolder annotation, in that case test will fetch the contract from the broker and verify the changes against it.

You can set up this test to run as part of the separate pipeline which will be triggered by the webhooks.

--

--