Consumer-driven contract testing

Charlie Brooking (@brookingcharlie)

Problem

Consider Consumer-Driven Contracts as a Design Pattern.

Problem statement:

How can a web service API reflect its clients' needs
while enabling evolution
and avoiding breaking clients?

[1] Ian Robinson, Consumer-Driven Contracts, Service Design Patterns

Options

How can we prevent breaking changes to services?

  1. Option 1: service providers create APIs that attempt to address all current and future consumer needs.
  2. Option 2: service providers write tests based on an interpretation of how consumers might use the service.
  3. Option 3: consumers provide documentation to providers specifying how they expect to use the service.
  4. Option 4: something else?

[1] Ian Robinson, Consumer-Driven Contracts, Service Design Patterns

Solution

  • Consumer developers write automated integration tests that express their expectations of a service API.
  • These tests are accepted by the service owner, who incorporates them into the service's test suite.

The set of integration tests received from all existing consumers represents the service's obligations to its consumer base.

[1] Ian Robinson, Consumer-Driven Contracts, Service Design Patterns

Evolving a service

  1. Consumer identifies need from provider that's not currently met.
  2. Consumer collaborates with provider to define API that satisfies new need whilst considering constraints and considerations of the provider.
  3. Provider implements API to honour agreed contract (and old ones).
  4. Provider runs automated test to verify that API satisfies the contract.
  5. Provider publishes service; consumer publishes contract tests.

Contracts codify the result of conversations - they don't replace them.

[1] Microservices: Consumer Driven Contracts in Practice

Context

Consumer-Driven Contracts don't make sense everywhere.

They should be used when:

  1. A service has several consumers, each with different needs.
  2. Service owners know who their consumers are.
  3. Client developers are able to communicate their expectations of the service's API to service owners.

[1] Ian Robinson, Consumer-Driven Contracts, Service Design Patterns

Frameworks

"The similarly named Pact and Pacto are two new open-source tools which allow testing interactions between service providers and consumers in isolation against a contract." [1]

[1] ThoughtWorks Technology Radar: Pact & Pacto

Pact: Consumer side

Service consumers define the requests they'll make and the responses they expect back. These expectations are used to run consumer tests against a mock service provider, recording interactions to a pact file.

[1] GitHub: realestate-com-au/pact

Pact: Provider side

Recorded interactions with the consumer are played back in the service provider tests to ensure the service provider actually does provide the response the consumer expects.

[1] GitHub: realestate-com-au/pact

Benefits of Pact

  • Allows testing of client and service using fast unit tests.
  • Mocking out service reduces likelihood of flakey tests.
  • Avoids need to run multiple services at the same time.
  • Standalone CI builds possible instead of integration environment.
  • Design of services is improved by being consumer-driven.

[1] GitHub: realestate-com-au/pact

Demo

Spring Boot Microservices + Pact-JVM


git clone git@github.com:brookingcharlie/microservices-pact.git

# Build/test the consumer
./gradlew microservices-pact-consumer:test
less microservices-pact-consumer/target/pacts/Foo_Consumer-Foo_Provider.json

# Build/test the provider
./gradlew microservices-pact-provider:assemble
./gradlew microservices-pact-provider:pactVerify

# Run the provider
java -jar microservices-pact-provider/build/libs/microservices-pact-provider-0.0.1.jar
# ... then in another window
curl -v 'http://localhost:8080/foos/'

Breaking the provider


--- a/microservices-pact-provider/src/main/java/io/pivotal/microservices/pact/provider/Application.java
+++ b/microservices-pact-provider/src/main/java/io/pivotal/microservices/pact/provider/Application.java
@@ -26,4 +26,4 @@ public class Application {
-    @RequestMapping(value = "/foos", method = RequestMethod.GET)
+    @RequestMapping(value = "/foos", method = RequestMethod.GET, produces = "application/json;charset=ASCII")
     public ResponseEntity> foos() {
         return new ResponseEntity<>(Arrays.asList(new Foo(42), new Foo(100)), HttpStatus.OK);
     }
--- a/microservices-pact-provider/src/main/java/io/pivotal/microservices/pact/provider/Foo.java
+++ b/microservices-pact-provider/src/main/java/io/pivotal/microservices/pact/provider/Foo.java
@@ -11,8 +11,8 @@ public class Foo {
-    public int getValue() {
+    public int getVal() {
         return value;
     }
 
-    public void setValue(int value) {
+    public void setVal(int value) {
         this.value = value;
     }
 }

patch -p1 -i break-provider.patch
./gradlew microservices-pact-provider:assemble microservices-pact-provider:pactVerify

Breaking the consumer


--- a/microservices-pact-consumer/src/main/java/io/pivotal/microservices/pact/consumer/ConsumerPort.java
+++ b/microservices-pact-consumer/src/main/java/io/pivotal/microservices/pact/consumer/ConsumerPort.java
@@ -23,6 +23,6 @@ public class ConsumerPort {
 
     public List foos() {
         ParameterizedTypeReference> responseType = new ParameterizedTypeReference>() {};
-        return restTemplate.exchange(url + "/foos", HttpMethod.GET, null, responseType).getBody();
+        return restTemplate.exchange(url + "/foos", HttpMethod.POST, null, responseType).getBody();
     }
 }
--- a/microservices-pact-consumer/src/test/java/io/pivotal/microservices/pact/consumer/ConsumerPortTest.java
+++ b/microservices-pact-consumer/src/test/java/io/pivotal/microservices/pact/consumer/ConsumerPortTest.java
@@ -23,6 +23,6 @@ public class ConsumerPortTest {
 
         return builder.uponReceiving("a request for Foos")
                 .path("/foos")
-                .method("GET")
+                .method("POST")
                 .willRespondWith()
                 .headers(headers)

patch -p1 -i break-consumer.patch
./gradlew microservices-pact-consumer:test microservices-pact-provider:pactVerify

Evolve the provider


--- a/microservices-pact-provider/src/main/java/io/pivotal/microservices/pact/provider/Foo.java
+++ b/microservices-pact-provider/src/main/java/io/pivotal/microservices/pact/provider/Foo.java
@@ -15,4 +15,8 @@ public class Foo {
     public void setValue(int value) {
         this.value = value;
     }
+
+    public String getExtra() {
+        return "123";
+    }
 }

patch -p1 -i evolve-provider.patch
./gradlew microservices-pact-provider:assemble microservices-pact-provider:pactVerify