The provided content discusses the use of Pact, a consumer-driven contract testing tool, for testing the integration of microservices, specifically focusing on the pub/sub communication pattern with examples using Spring Boot microservices.
Abstract
Pact is a tool that facilitates consumer-driven contract testing, ensuring that applications communicate effectively by adhering to agreed-upon contracts. The article delves into the application of Pact for contract testing in a microservices architecture, particularly when using the pub/sub messaging pattern. It explains the benefits of using Pact, such as testing only the parts of communication that are actually used by consumers, thus allowing for changes in provider behavior that are not used by current consumers. The content includes a detailed guide on how to add Pact dependency libraries to a project, test consumer and publisher applications, and publish and verify contracts using Pact Broker. Practical examples and code snippets are provided for implementing Pact testing with Spring Boot microservices, demonstrating how to handle multiple events in a pub/sub scenario.
Opinions
The author emphasizes the advantage of Pact in focusing on the actual interactions between services, which can prevent unnecessary test breakages and allow for more flexible service evolution.
Pact is presented as particularly useful in a microservices environment with many services, helping developers avoid the challenges of versioning hell.
The use of Pact for pub/sub contract testing is highlighted as a strong use case for the tool, showcasing its versatility beyond simple request/response HTTP interactions.
The article suggests that Pact's approach to contract testing, which involves defining interactions in code and generating contracts during test execution, is a superior method compared to manually writing and maintaining contracts.
The author provides a positive assessment of Pact's integration with Spring Boot and its support for JUnit 5, indicating that it fits well within the Java ecosystem for testing microservices.
The inclusion of detailed instructions and code examples reflects the author's opinion that Pact is a practical and accessible tool for developers looking to implement contract testing in their projects.
Pact is a code-first consumer-driven contract testing tool. The contract is generated during the execution of the automated consumer tests.
Contract testing is a technique for testing an integration point by checking each application in isolation to ensure the messages it sends or receives conform to a shared understanding that is documented in a “contract”.
For applications that communicate via HTTP, these “messages” would be the HTTP request and response, and for an application that used queues, this would be the message that goes on the queue. For this story, we will focus on deep diving into how to use Pact for pub/sub contract testing. Let’s use two microservices, customer-service and order-service, as examples to demonstrate how Pact testing is done in Spring Boot microservices. In this example, three customer related events are published from customer-service, order-service subscribes to the topic those events are published to, and it consumes those events.
In practice, a common way of implementing contract tests (and the way Pact does it) is to check that all the calls to your test doubles return the same results as a call to the real application would.
A major advantage of Pact is that only parts of the communication that are actually used by the consumer(s) get tested. This in turn means that any provider behavior not used by current consumers is free to change without breaking tests.
Contract testing is immediately applicable when we have two services that need to communicate — such as pub/sub. Although a single client and a single service is a common use case, contract testing really shines in an environment with many services (as is common for a microservice architecture). Having well-formed contract tests makes it easy for developers to avoid version hell.
Add Pact dependency libs to project pom
Libraries are available from maven central. Periodically check for upgraded versions for your project use.
Since we’re doing consumer-driven contract testing, let’s start with the consumer side. We use Dapr pub/sub component in our example microservice, order-service. SubscriberController has an endpoint consumeCustomerCrudEvent to listen on the topic defined in @Topic annotation. Dapr sidecar wraps the event in CloudEvent format. SubscriberController takes a CloudEvent as input, transforms it into one of the three customer events with the help of ObjectMapper. The event is then handled by OrderService to persist its data into the database. The whole class looks like this:
To test the SubscriberController, we create a unit test using Pact, SubscriberControllerTest:
Let’s break down the test line by line:
@ExtendWith(PactConsumerTestExt.class)
Write Pact consumer tests with JUnit 5, you need to add @ExtendWith(PactConsumerTestExt) to your test class. This is similar to JUnit 4’s @RunWith(SpringRunner.class).
@PactTestFor(providerName = "customer-service", port = "9100", providerType = ProviderType.ASYNCH)
@PactTestFor annotation tells the Pact extension how to set up the Pact test. You can either put this annotation on the test class, or on the test method. The @PactTestFor annotation allows you to control the mock server by specifying the provider. It allows you to set the hostname to bind to (default is localhost) and the provider app’s port. The mock server runs in the same JVM as the test. If you set the providerName on the @PactTestFor annotation, then the first method with a @Pact annotation with the same provider name will be used. If you set the pactMethod on the @PactTestFor annotation, then the method with the provided name will be used (it still needs a @Pact annotation).
@SpringBootTest
@SpringBootTest lets Spring create a SubscriberController and @Autowire it into our test.
@Pact(consumer = "order-service")
The contract itself is defined in the method annotated with @Pact. For each test, you need to define a method annotated with the @Pact annotation that returns the interactions for the test.
The DSL has the following pattern:
The MessagePactBuilder builds the Pact and returns a MessagePact. hasPactWith defines the message provider. expectsToReceive expects a test case by providing the request description, which will have corresponding test verification in the provider Pact test. We use LambdaDsl to construct the body for the content. In this case, the object contains only three attributes “customerId”, of type UUID .uuid(String name, UUID example) , firstName and lastName, both of type String. We pass in a sample UUID and test values for the first and last name attributes. For a list of DSL matching methods, refer to Pact consumer | Pact Docs. withMetadata defines metadata such as pub/sub topic, routes, etc.
For the test verification, we specify the pactMethod pointing to the Pact method annotated with @Pact, and we simply pass the MessagePact provided by Pact into the consumer and if there is no exception, we assume that the consumer can handle the message.
Testing the Publisher
The publisher test verifies the contract. See CustomerEventsPublishPactVerificationTest below.
Main points explained:
@PactFolder("pacts")
We can use this annotation for local pact testing, with pact file located under resources’ “pacts” folder as specified.
Alternatively, we can specify details of the pact broker where we’d like to publish our pact file by using annotation:
We can set the test target, MessageTestTarget, the object that defines the target of the test, which should point to the provider, on the PactVerificationContext. We need to do this in a before test method (annotated with @BeforeEach). We also mock the CustomerService here.
For writing Pact verification tests with JUnit 5, there is an JUnit 5 Invocation Context Provider that you can use with the @TestTemplate annotation. This will generate a test for each interaction found for the Pact files for the provider.
To use it, add the @Provider and one of the Pact source annotations to your test class, then add a method annotated with @TestTemplate and @ExtendWith(PactVerificationInvocationContextProvider.class) that takes a PactVerificationContext parameter. You will need to call verifyInteraction() on the context parameter in your test template method.
@PactVerifyProvider("valid CustomerWasCreated from provider")
For each interaction in the contract, we need a method annotated with @PactVerifyProvider, in our case we have three interactions. In this method, we use jMock to mock the customerService and then pass to it the customerInfo. We convert the event into JSON string using ObjectMapper and return it.
Pact will automatically send the produced string message to the MessageTestTarget in the @BeforeEach block, where it will be checked against the contract.
Testing Multiple Events
In reality, we often run into multiple events pub/sub scenarios. Let’s take a look at how to implement Pact testing for multiple events on both the consumer side and the provider side.
Testing Impl on the Consumer Side
The only difference between testing a single event and testing multiple events is when the Pact message is built. No, we don’t need a separate Pact for each and every event. We can combine multiple events in one single Pact. A message/event Pact interaction may contain multiple messages/events by invoking expectsToReceive().withContent() more than once. For example, we consume three types of events on the consumer side:
CustomerWasCreated
CustomerWasUpdated
CustomerWasDeleted
The Pact in this case would be constructed as follows.
For test verification, we create corresponding tests for the above three events, referring to the same pactMethod which defined the Pact for those three events. Notice they are almost identical except for the message index when getting it from messagePact, and the data prep line for both the customer update and delete tests.
Testing Impl on the Publisher Side
Provider side is more straightforward. Each interaction in the Pact contract needs a method annotated with @PactVerifyProvider. We have three interactions in our Pact, so we create three methods annotated with @PactVerifyProvider.
Using Pact Broker to Publish and Verify Pact
By default, Pact files (the contracts) are written to target/pacts within your app’s module or submodule. We can publish the Pact contracts to a Pact broker such as PactFlow.
Add maven plugin
We only need to add the following plugin in our app’s pom. Notice the details of Pact broker url, Pact broker token. It is the read/write token generated by PactFlow, associated with your PactFlow account, generated when you first sign up with PactFlow.
Run maven command to publish the contract
Once maven plugin is defined, run mvn pact:publish to publish the contract to PactFlow. Or, you can run mvn clean install pact:publish to build, test your app and publish your Pact contract all at once.
Run provider verification test
Run mvn clean install on your provider app to build and test it. Upon successful verification of provider test, PactFlow shows “Successfully verified”.