What’s up Guys! Welcome to automationcalling.com
In today’s world, most of the application is being developed in Microservice architecture irrespective of technology and language, in fact, some of the existing application in the world almost migrated from Monolithic to Microservice Architecture.
To Test Microservice architecture is always interesting and challenging for eg., It’s not only involving individual services exposing REST API call but also how service is interacting with external service all, messaging queues, Caching DB, Storage DB like SQL/No SQL etc., in either Synchronous/Asynchronous way execution.
According to Test Automation Pyramid Model, mostly we cover Unit, Integration, UI Test which we adopt different testing terminology like API/Backend test, Contract Test for Integration, Acceptance test applicable for Integration as well as UI test, Acceptance/End to End/User Journey test applicable for UI etc., Unfortunately we don’t have stable testing terminology in testing but it all depends on how we efficiently use for our project requirements.
In this blog, we are going to take a look at what is Contract test? and how we can implement it
Introduction
The contract test is also called the “Consumer-Driven Contract (CDC)” is to test integration between Consumer and Provider side. In simple words, To create a Contract file in the Consumer side regarding how the API endpoints request header, body, response status code, the response message, response header, etc., after that the consumer share the contract file to the provider to inform how they supposed to be consumed. This will help to avoid integration defects at earliest and most importantly consumer code doesn’t break.
The below diagram depicts what are the real-time challenges:
Diagram 1: External Service
Diagram 2: Service Dependency
To address this challenge, we are going to take a look at a tool called PACT
What is PACT?
PACT is a DSL library that supports test integration between the Consumer and Provider side. PACT supports to develop a mock test on the consumer side; when you run a test from the client-side, it generates a PACT file that should be shared to the Provider side to run the test to make sure no issues.
What is Not good for PACT?
- API Functional Test/Business logic
- Performance/Load Test
- Security test
- A situation where you share PACT file to Provider for eg., Public API
How to do a Contract Test Using PACT?
PACT supports multiple languages like Ruby, JVM, .NET, Javascript, Go and Python to run contract tests. In this example, we are going to take a look at JVM.
Before starting the test make sure to include the following dependencies in your pom.xml (Maven Project).
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-consumer-junit</artifactId>
<version>
4.0
.
2
</version>
</dependency>
<dependency>
<groupId>au.com.dius</groupId>
<artifactId>pact-jvm-provider-spring</artifactId>
<version>
4.0
.
2
</version>
</dependency>
In this blog, we are going to use a sample spring boot application, which provides employee details
I’m going to expose Rest API endpoints from spring boot application and explaining how to write consumer and provider tests.
The below diagram depicts interaction between consumer and consumer mock.
Consumer Mock
PACT DSL offers to develop Consumer Test and Provider test. In order to that, first we need to create consumer mock.
Consumer Mock is nothing, it’s similar like stub which produces the same results again and again. The response must be similar like how you get a response from real service.
Before start writing code, first, you must set the ProviderRule as mentioned below, which acts like a local server and produce a response.
@Rule public PactProviderRule mockProvider = new PactProviderRule("Provider_Test", "localhost", 9080, this);
The below code defines, what are the request parameters and response parameters supposed to be sent and receive from your real services. This should match exactly the same as real services (including authentication or authorization).
@Pact(consumer = "Consumer_Test") public RequestResponsePact createPact(PactDslWithProvider builder) { Map<String, String> headers = new HashMap<>(); headers.put("Content-Type", "application/json"); return builder .given("Test GET Request") .uponReceiving("GET REQUEST") .path("/employees") .method("GET") .willRespondWith() .status(200) .headers(headers) .body("[\n" + " {\n" + " \"id\": \"1\",\n" + " \"name\": \"Muthu\",\n" + " \"dept\": \"Development\"\n" + " }\n" + "]").toPact(); }
Remember, “PactDslWithProvider” is a builder pattern and end of the your method, you have to set “.toPact()” method.
All setup related to mock is done, it’s time to run Consumer Test
Consumer Test
In your consumer test, make sure you extend Consumer Mock, this is required because you are hitting local server endpoints that response to what is supposed to be returned.
public class ClientTest extends ConsumerMock { @Test @PactVerification() public void invokeMokeusingClient() { ResponseEntity<String> response = new RestTemplate() .getForEntity(mockProvider.getUrl() + "/employees", String.class); Assert.assertEquals(response.getStatusCodeValue(), 200); Assert.assertTrue(response.getHeaders().get("Content-Type").contains("application/json")); } }
After running the Consumer Test, the test must be successful and the PACT file gets generated in JSON format under target/mypacts/.
Here is the format of example PACT file:
{ "provider": { "name": "Provider_Test" }, "consumer": { "name": "Consumer_Test" }, "interactions": [ { "description": "GET REQUEST", "request": { "method": "GET", "path": "/employees" }, "response": { "status": 200, "headers": { "Content-Type": "application/json" }, "body": [ { "id": "1", "name": "Muthu", "dept": "Development" } ] }, "providerStates": [ { "name": "Test GET Request" } ] } ], "metadata": { "pactSpecification": { "version": "3.0.0" }, "pact-jvm": { "version": "4.0.2" } } }
Now the Consumer side, the test is over and everything looks fine meaning contract file gets created with expected request and response.
I’m using Consumer and Provider tests in the same sample project but in reality, Consumers may be different teams or providers may be different teams.
Provider Test
Copy the PACT file from “targets/mypacts” to paste under “src/main/resources/pacts”. As this is the same project, we easily copy the file, if it’s a different project there is option called PACT Broker this can be set up in CI as well as Docker. I will explain detail in another blog.
After copied PACT file, create the following class to run Provider Test.
@RunWith(PactRunner.class) @Provider("Provider_Test") @PactFolder("pacts") public class ProviderVerification { @State("Test GET Request") public void toGetState() { } @TestTarget //public final Target target = new HttpTarget("http", "localhost", 8080, "/employees"); public final Target target=new HttpTarget(8080);
Note:
- @State(“Test GET Request”) is the test which is defined in the PACT file
- If you stop your application or service and run provider test, it gets failed because it ran against real service and compare the response.
- public final Target target=new HttpTarget(8080), port is sufficient if your application is running in local otherwise you can pass host, http type etc.,
After successful running, the following response received.
09:30:48.320 [main] DEBUG au.com.dius.pact.core.matchers.HeaderMatcher – Comparing header ‘Content-Type’: ‘application/json’ to ‘application/json’
09:30:48.320 [main] DEBUG au.com.dius.pact.core.matchers.HeaderMatcher – Comparing content type header: ‘application/json’ to ‘application/json’
09:30:48.336 [main] DEBUG au.com.dius.pact.core.matchers.Matching – Found a matcher for application/json -> au.com.dius.pact.core.matchers.JsonBodyMatcher@319bc845
09:30:48.336 [main] DEBUG au.com.dius.pact.core.matchers.JsonBodyMatcher – compareValues: No matcher defined for path [$, 0, id], using equality
09:30:48.336 [main] DEBUG au.com.dius.pact.core.matchers.JsonBodyMatcher – compareValues: No matcher defined for path [$, 0, name], using equality
09:30:48.351 [main] DEBUG au.com.dius.pact.core.matchers.JsonBodyMatcher – compareValues: No matcher defined for path [$, 0, dept], using equality
returns a response which
has status code 200 (OK)
has a matching body (OK)
09:30:48.386 [main] DEBUG au.com.dius.pact.provider.DefaultTestResultAccumulator – Received test result ‘au.com.dius.pact.core.pactbroker.TestResult$Ok@54d18072’ for Pact Provider_Test-Consumer_Test and GET REQUEST
09:30:48.386 [main] DEBUG au.com.dius.pact.provider.DefaultTestResultAccumulator – Number of interactions #1 and results: [au.com.dius.pact.core.pactbroker.TestResult$Ok@54d18072]
09:30:48.389 [main] DEBUG au.com.dius.pact.provider.DefaultTestResultAccumulator – All interactions for Pact Provider_Test-Consumer_Test have a verification result
09:30:48.389 [main] WARN au.com.dius.pact.provider.DefaultTestResultAccumulator – Skipping publishing of verification results as it has been disabled (pact.verifier.publishResults is not ‘true’)
How to handle Dynamic Response in Contract Test
There are some challenges like timestamp, unique id, random id, etc., which can be generated in API response. To handle this, we should use a matching pattern .
Example keywords:
- matcher: /\d{2}\/\d{2}\/\d{4}/)
- age: Pact.like(73)
- each_like(name: “Fred”, age: 2)
This will eliminate in comparing between real service response and response defined in contract file.
Conclusion
PACT is a fantastic tool to run an integration test between Consumer and Provider. In fact, this tool helps not only detect integration defects but also helps defect prevention at the earliest stage before breaking consumer code. I strongly recommend PACT DSL.
Please refer the GitHub which has Spring Boot Application and example test, remember you need to start “EmployeeApp” first
Thanks for your time, please do subscribe for more updates!
Leave a Reply