Microservices and Consumer Driven Contract testing using Pact

Share on:

Background

As per the current trends, Microservice Architecture has become a common paradigm using which enterprise applications are built. With this paradigm shift, an application is going to have myriad set of independent and autonomous (micro)services. So how does a developer do testing within Microservice Architecture? Answer is very obvious -

  1. Create integration tests that invokes microservice under test which will internally call dependent microservices
  2. Get all the services under test up and running
  3. Start executing integration test which invokes microservice under test

With this approach, the entire developer level integration testing will have following disadvantages -

  • Extremely slow execution of integration tests
  • Fragile tests as they are directly dependent on successful execution of other micorservices
  • Obscures ability to debug outcome of tests as it will be ambiguous
  • Combinatorial increase in number of tests (depending on number of classes and alternate paths within them)

And hence the overall intent behind having integration tests gets defeated.

So Martin Fowler gave an interesting perspective called Consumer Driven Contract which is nothing but a contract between consumer of service and producer of service. In this style of testing, format of contract is defined by Consumer and then shared with the corresponding Producer of service.

Lets take an example to understand this. We have ReservationClient which is an end consumer facing API; this in turn invokes ReservationService which is responsible for managing business workflows pertaining to Reservation. For the sake of simplicity, we will just try to retrieve Reservation using both the services.

Pact

We will be using Pact for realizing Consumer Driven Contract (CDC) testing. PACT is an open source CDC testing framework which also supports multiple languages like Ruby, Java, Scala, .NET, Javascript, Swift/Objective-C. It comprises of 2 main steps for performing CDC testing. One may want to look at the jargons to understand Pact specific terminology

  1. Pact generation on consumer side service
  2. Pact verification on provider side service

1. Pact Generation on Consumer side

1.1 Define expected result

We first start by writing JUnit test case TestReservationClient for ReservationClient, where we specify the expected result from the Provider i.e. ReservationService. Test case can be implemented in 2 ways -

  1. Extend the base class ConsumerPactTest
  2. Use annotations

We will be implementing test case using second approach.

 1@Rule
 2public PactProviderRule pactProviderRule = new PactProviderRule("reservation-provider-demo", this);
 3
 4	@Pact(consumer = "reservation-consumer-demo")
 5	public PactFragment createFragment(PactDslWithProvider pactDslWithProvider) {
 6		Map<String, String> headers = new HashMap<>();
 7		headers.put("Content-Type", "application/json;charset=UTF-8");
 8		return pactDslWithProvider.given("test demo first state")
 9				.uponReceiving("ReservationDemoTest interaction")
10					.path("/producer/reservation/names")
11					.method("GET")
12				.willRespondWith()
13					.status(200)
14					.headers(headers)
15					.body("{" + 
16								"\\"name\\" : \\"Dhaval\\"" + 
17					"}")
18					.toFragment();
19	}
20
21	@Test
22	@PactVerification
23	public void runTest() throws Exception {
24		String url = pactProviderRule.getConfig().url();
25		Reservation fetchedReservation = new ReservationAPIGateway(url+"/producer/reservation/names").fetchOne();
26		assertEquals("Dhaval", fetchedReservation.getName());
27	}

1.2 Generate the Pact file

Lets run the test case after implementing test case with required Pact contracts. If the test runs successfully, a JSON file will be created within a new folder pacts underneath /target folder

 1{
 2    "provider": {
 3        "name": "reservation-provider-demo"
 4    },
 5    "consumer": {
 6        "name": "reservation-consumer-demo"
 7    },
 8    "interactions": \[
 9        {
10            "description": "ReservationDemoTest interaction",
11            "request": {
12                "method": "GET",
13                "path": "/producer/reservation/names"
14            },
15            "response": {
16                "status": 200,
17                "headers": {
18                    "Content-Type": "application/json;charset=UTF-8"
19                },
20                "body": {
21                    "name": "Dhaval"
22                }
23            },
24            "providerState": "test demo first state"
25        }
26    \],
27    "metadata": {
28        "pact-specification": {
29            "version": "2.0.0"
30        },
31        "pact-jvm": {
32            "version": "3.3.3"
33        }
34    }
35}

1.3 Share the generated pact file with Producer

Last step of consumer should be to share the generated contract with provider. This can be done either by a file sharing service or Pact Broker

2. Pact Verification on Provider side

2.1 Bootstrap the Provider service

After getting the pact file from consumer, provider service i.e. ReservationServiceController should be bootstrapped first

2.2 Execute JUnit-Pact test against the

Implement a JUnit test which will refer to the pact file and also match its state with that of the state configured at Consumer's end

 1@RunWith(PactRunner.class)
 2@Provider("reservation-provider-demo")
 3@PactFolder("../reservation-client/target/pacts")
 4@VerificationReports({"console", "markdown"})
 5public class ReservationServiceControllerContractTest {
 6	@TestTarget
 7    public final Target target = new HttpTarget(8080);
 8
 9    @BeforeClass
10    public static void setUpProvider() {
11
12    }
13
14    @State("test demo first state")
15    public void demoState() {
16        System.out.println("Reservation Service is in demo state");
17    }
18}

2.3 Check verification result

After executing verification test by Provider JUnit i.e. ReservationServiceControllerContractTest we get the output as shown below

 1Reservation Service is in demo state
 2
 3Verifying a pact between reservation-consumer-demo and reservation-provider-demo
 4  Given test demo first state
 5  ReservationDemoTest interaction
 601:29:28.559 \[main\] DEBUG au.com.dius.pact.provider.ProviderClient - Making request for provider au.com.dius.pact.provider.ProviderInfo(http, localhost, 8080, /, reservation-provider-demo, null, null, null, null, null, false, null, changeit, null, true, false, null, \[\], \[\]):
 701:29:28.617 \[main\] DEBUG au.com.dius.pact.provider.ProviderClient - 	method: GET
 8	path: /producer/reservation/names
 9	query: null
10	headers: null
11	matchers: \[:\]
12	body: au.com.dius.pact.model.OptionalBody(MISSING, null)
13
14...
15..
16.
17Reservation Service is in demo state
18
19Verifying a pact between reservation-consumer-demo and reservation-provider-demo
20  Given test demo first state
21  ReservationDemoTest interaction
2201:29:28.559 \[main\] DEBUG au.com.dius.pact.provider.ProviderClient - Making request for provider au.com.dius.pact.provider.ProviderInfo(http, localhost, 8080, /, reservation-provider-demo, null, null, null, null, null, false, null, changeit, null, true, false, null, \[\], \[\]):
2301:29:28.617 \[main\] DEBUG au.com.dius.pact.provider.ProviderClient - 	method: GET
24	path: /producer/reservation/names
25	query: null
26	headers: null
27	matchers: \[:\]
28	body: au.com.dius.pact.model.OptionalBody(MISSING, null)

Lets understand the nature of output in case contract fails. Lets create a separate JUnit for ReservationClient which returns user's name along with its id. We then follow the same steps as mentioned above and then verify the generated pact with Provider's JUnit test case. Resulting output is as shown below

 1Reservation Service is in demo state
 2
 3Verifying a pact between reservation-consumer-demo and reservation-provider-demo
 4  Given test demo first state
 5  ReservationDemoTest interaction
 601:45:42.938 \[main\] DEBUG au.com.dius.pact.provider.ProviderClient - Making request for provider au.com.dius.pact.provider.ProviderInfo(http, localhost, 8080, /, reservation-provider-demo, null, null, null, null, null, false, null, changeit, null, true, false, null, \[\], \[\]):
 701:45:43.078 \[main\] DEBUG au.com.dius.pact.provider.ProviderClient - 	method: GET
 8	path: /producer/reservation/names
 9	query: null
10	headers: null
11	matchers: \[:\]
12	body: au.com.dius.pact.model.OptionalBody(MISSING, null)
1301:45:44.729 \[main\] DEBUG org.apache.http.client.protocol.RequestAddCookies - CookieSpec selected: default
14
15...
16..
17.
18
1901:45:46.963 \[Finalizer\] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection manager shut down
2001:45:47.374 \[main\] DEBUG au.com.dius.pact.model.Matching$ - Found a matcher for application/json -> Some((application/.\*json,au.com.dius.pact.matchers.JsonBodyMatcher@71e5f61d))
2101:45:47.423 \[main\] DEBUG au.com.dius.pact.matchers.JsonBodyMatcher - compareValues: No matcher defined for path List($, body, name), using equality
22    returns a response which
23      has status code 200 (OK)
24      includes headers
25        "Content-Type" with value "application/json;charset=UTF-8" (OK)
26      has a matching body (FAILED)
27
28Failures:
29
300) ReservationDemoTest interaction returns a response which has a matching body
31      $.body -> Expected id='100' but was missing
32
33      Diff:
34
35      @1
36      -    "name": "Dhaval",
37      -    "id": "100"
38      +    "name": "Dhaval"
39      }

It is quiet evident from the above error that Consumer has broken the contract as it is sending id attribute of user.

Summary

As we saw above, with CDC we can make integration testing easy to manage and strive to get faster feedback. This in a way helps us to realize Test Pyramid. In subsequent posts, I will try to introduce usage of Spring Cloud Contract for realizing CDC testing.

Github code for your perusal.

comments powered by Disqus