CDC testing with Pact + Spring
Overview
Microservices is a hot topic in software development circles in the last few years. A lot of companies if not the most are migrating to a microservice architecture nowadays. Now a big portion of those companies does actually fail to do so during the migration, which is understandable because it's not an easy task. There are requirements and challenges to consider before making that choice and some of those are:
- Basic monitoring
- Rapid provisioning
- Rapid app deployment
- DevOps culture
Testing microservices is one of the challenges that a lot of companies tackle poorly which makes them move slow in producing value, or even break production. Honestly, it's a really hard task to accomplish, because as the number of services goes up and they all changing things independently from one another deploying all the time, it can become really difficult to keep up with this graph of ever-changing relationship.
Consumer-Driven Contract testing is a really good solution to a lot of problems in a microservice architecture. Consumer-driven contract testing is a type of contract testing that ensures that a provider is compatible with the expectations that the consumer has of it.
What is Pact?
Pact is a code-first tool for testing HTTP and message integrations using contract tests. Contract tests assert that inter-application messages conform to a shared understanding that is documented in a contract.
Without contract testing, the only way to ensure that applications will work correctly together is by using expensive and brittle integration tests.
Contract tests are: trustworthy, fast, reliable, targeted, cheap. You can find a cool slideshow of how pact works here.
Showcase
In this blog post, we will build a project with two services, one service that will act as a provider, and one service that will act as a consumer of the provider. We will isolate those services with different modules. The provider service will expose an HTTP endpoint that the rest of the services will consume by calling the provider's REST API.
The provider will expose a GET endpoint at /post/{id}
and it will return a post object like the code below.
$ curl --request GET 'localhost:8080/post/1' | jq{ "id": 1, "author": "Bob", "text": "Second post text"}
Let's build the consumer
Production Code
The code of the consumer is pretty simple, we just have a simple GET endpoint that fetches a specific post from the provider service using the RestTemplate
.
@RestControllerpublic class ClientController { @GetMapping("/consume1") public ResponseEntity<String> consume1() { return new RestTemplate().getForEntity("http://localhost:8080/post/1", String.class); }}
Define the contract.
Before we create the contract, we need to add the following dependency in the consumer's pom file.
<dependency> <groupId>au.com.dius</groupId> <artifactId>pact-jvm-consumer-junit_2.11</artifactId> <version>3.5.0</version> <scope>test</scope></dependency>
Then we need to define the contract between the consumer and the provider. This piece of code can live inside the testing class.
@Rule public PactProviderRuleMk2 mockProvider = new PactProviderRuleMk2("test_provider", "localhost", 8080, this);@Pact(consumer = "consumer1")public RequestResponsePact createPactContract(PactDslWithProvider builder) { Map<String, String> headers = Map.of("Content-Type", "application/json"); PactDslJsonBody body = new PactDslJsonBody() .numberType("id", 1) .stringType("author", "Alice") .stringType("text", "First post text"); return builder .given("post exists") .uponReceiving("get a single post request") .path("/post/1") .method("GET") .willRespondWith() .status(200) .headers(headers) .body(body) .toPact();}
Refer to the official documentation for more information.
Test the consumer
One more thing to be configured is the location of where our pact artefacts should be saved. We can do this by configuring the maven-surefire-plugin
like the below snippet.
<plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <systemPropertyVariables> <pact.rootDir>../pactFiles</pact.rootDir> </systemPropertyVariables> </configuration> </plugin></plugins>
Last but not least, to produce the pact artefact we need a test. We can have a test to verify that the GET method works properly based on the contract we defined above. An example of such a test can be like the following.
@Test@PactVerification("test_provider")public void givenGet_whenSendRequest_shouldReturn200WithProperHeaderAndBod() { ResponseEntity<String> response = new ClientController().consume1(); assertThat(response.getStatusCode().value()).isEqualTo(200); assertThat(response.getHeaders().get("Content-Type").contains("applicationjson")).isTrue(); assertThat(response.getBody()).contains("id", "1", "author", "Alice", "text", "First post text");}
If we run this test we will get the pact artefact, a JSON file named consumer1-test_provider.json
under the pactFiles
folder.
Let's build the provider
Production Code
The provider service exposes an API that returns all the posts stored (in-memory). Basically, it will return POST_1
if 1
is passed as an id to the endpoint and POST_2
if 2
is passed as an id. In any other case, we will get a bad request.
@RestControllerpublic class ProviderController { public static final Post POST_1 = new Post(1, "Alice", "First post text"); public static final Post POST_2 = new Post(2, "Bob", "Second post text"); public final List<Post> POSTS = asList(POST_1, POST_2); @GetMapping(value = "/post/{postId}", produces = "application/json") public ResponseEntity<Post> retrievePost(@PathVariable Integer postId) { try { final Post post = POSTS.get(postId); return new ResponseEntity<>(post, HttpStatus.OK); } catch (IndexOutOfBoundsException e) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } }}
Test the provider
Like previously on the consumer side, we need to add a dependency in the provider`s pom file.
<dependency> <groupId>au.com.dius</groupId> <artifactId>pact-jvm-provider-junit_2.11</artifactId> <version>3.5.0</version> <scope>test</scope></dependency>
To test the provider against the pact artefact we need to write the following test
@RunWith(PactRunner.class)@Provider("test_provider")@PactFolder("../pactFiles")public class PactProviderTest { @TestTarget public final Target target = new HttpTarget("http", "localhost", 8080, "/"); @BeforeClass public static void start() { ConfigurableWebApplicationContext application = (ConfigurableWebApplicationContext) SpringApplication.run(ProviderApplication.class); } @State("post exists") public void postExists() { }}
Running the test should pass verifying that all the assumptions the consumer made are valid from the provider's side. We now have successfully tested the interaction between the two services using CDC with Pact.
Sharing the Pact artefact
There are multiple ways to share pact artefacts. Obviously, in this project, we are sharing the artefacts through the file system. However, that's not the only option. There are three ways to share the pact files:
- File System (which is used in this project)
- URL
- Pact Broker
Closing the loop
You could argue how to test services that don't co-live in the same repository. That's a perfectly valid point. In this project, it's pretty straight forward to test both ends since we have the code in the same repository. It's obvious that if any change has been made to any of those services at the interface level then we have to make sure that the interface is still respected by both ends before we make any deployment. Now, this might be tricky if we don't have those services in the same repository (which is most likely), but there are ways to solve that issue.
There are four ways we can test that nothing has been broken after a change to either the consumer or the provider.
- Manually testing it
- Directly (if both consumer and provider live in the same repository)
- Pact Broker webhook
- Swagger validator
Closing notes
An important concept that is not covered in this blog post is pact states. Covering this might actually increase the complexity of this tutorial so I thought we could skip it (although it is important) and if you are interested you can find more here.
Pact supports most of the popular languages, you can find the full list here. It's worth mentioning that an alternative to Pact is Spring Cloud Contract.
You can find the project we build in this blog post in my Github profile here. The project has two more consumers and hence two more pact files defining the contract between these and the provider.
Subscribe to my newsletter
An irregular digest about tech, software, and mentoring.