Skip to content

Mikuu/Pact-JVM-Example

Repository files navigation

PACT JVM Example

These codes provide an example about how to do Contract Test with PACT JVM Junit, which uses Junit in Consumer side and Gradle task in Provider side, it will cover:

  • Microservices examples created with Spring Boot.
  • Example of one Provider to two Consumers.
  • Write Consumer tests in different ways including using Basic Junit, Junit Rule and DSL method.
  • Example of Provider State.
  • Example of utilizing Pact Broker.

Contents

1. Understand The Example Applications

Clone the codes to your local, then you can find:

1.1. Example Provider

This is an API backend service which serves at http://localhost:8080/information, consumers can retrieve some person information by calling this endpoint with a query parameter name, to start the provider:

./gradlew :example-provider:bootRun

then call http://localhost:8080/information?name=Miku will get:

and call http://localhost:8080/information?name=Nanoha will get:

1.2. Example Consumer Miku

This is the first example consumer we called Miku, to start it:

./gradlew :example-consumer-miku:bootRun

then visit http://localhost:8081/miku in your browser, you can get this:

compare with Provider's payload and the information on the web page, you can find that the attributes salary and nationality are not used by Miku.

1.3. Example Consumer Nanoha

This is the second example consumer we called Nanoha, to start it:

./gradlew :example-consumer-nanoha:bootRun

then visit http://localhost:8082/nanoha in your browser, you can get this:

image

similar to Miku, Nanoha does not use the attribute salary neither but uses attribute nationality, so this is a little difference between the two consumers when consuming the response from the Provider's same endpoint.

2. Contract Test between Provider and Consumer Miku

Now, it's time to look into the tests.

This README will not go through all tests line by line, because the tests themselves are very simple and straightforward, so I will only point out some highlights for each test. For detailed explanation about the codes, please refer the official document

2.1. Create Test Cases

By the time this example is created, PACT JVM Junit provides 3 ways to write the pact test file at consumer side, the Basic Junit, the Junit Rule and Junit DSL.

2.1.1. Basic Junit

PactBaseConsumerTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class PactBaseConsumerTest extends ConsumerPactTest {

    @Autowired
    ProviderService providerService;

    @Override
    @Pact(provider="ExampleProvider", consumer="BaseConsumer")
    public RequestResponsePact createPact(PactDslWithProvider builder) {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json;charset=UTF-8");

        return builder
                .given("")
                .uponReceiving("Pact JVM example Pact interaction")
                .path("/information")
                .query("name=Miku")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body("{\n" +
                        "    \"salary\": 45000,\n" +
                        "    \"name\": \"Hatsune Miku\",\n" +
                        "    \"nationality\": \"Japan\",\n" +
                        "    \"contact\": {\n" +
                        "        \"Email\": \"hatsune.miku@ariman.com\",\n" +
                        "        \"Phone Number\": \"9090950\"\n" +
                        "    }\n" +
                        "}")

                .toPact();
    }

    @Override
    protected String providerName() {
        return "ExampleProvider";
    }

    @Override
    protected String consumerName() {
        return "BaseConsumer";
    }

    @Override
    protected void runTest(MockServer mockServer, PactTestExecutionContext context) {
        providerService.setBackendURL(mockServer.getUrl());
        Information information = providerService.getInformation();
        assertEquals(information.getName(), "Hatsune Miku");
    }
}

The providerService is the same one used in consumer Miku, we just use it to do a self integration test, the purpose for this is to check if consumer Miku can handle the mocked response correctly, then ensure the Pact content created are just as we need before we send it to Provider.

mockServer.getUrl() can return the mock server's url, which is to be used in our handler.

2.1.2. Junit Rule

PactJunitRuleTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class PactJunitRuleTest {

    @Autowired
    ProviderService providerService;

    @Rule
    public PactProviderRule mockProvider = new PactProviderRule("ExampleProvider", this);

    @Pact(consumer = "JunitRuleConsumer")
    public RequestResponsePact createPact(PactDslWithProvider builder) {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json;charset=UTF-8");

        return builder
                .given("")
                .uponReceiving("Pact JVM example Pact interaction")
                .path("/information")
                .query("name=Miku")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body("{\n" +
                        "    \"salary\": 45000,\n" +
                        "    \"name\": \"Hatsune Miku\",\n" +
                        "    \"nationality\": \"Japan\",\n" +
                        "    \"contact\": {\n" +
                        "        \"Email\": \"hatsune.miku@ariman.com\",\n" +
                        "        \"Phone Number\": \"9090950\"\n" +
                        "    }\n" +
                        "}")
                .toPact();
    }

    @Test
    @PactVerification
    public void runTest() {
        providerService.setBackendURL(mockProvider.getUrl());
        Information information = providerService.getInformation();
        assertEquals(information.getName(), "Hatsune Miku");
    }
}

This test uses Junit Rule which can simplify the writing of test cases comparing with the Basic Junit.

PactJunitRuleMultipleInteractionsTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class PactJunitRuleMultipleInteractionsTest {

    @Autowired
    ProviderService providerService;
    
    @Rule
    public PactProviderRule mockProvider = new PactProviderRule("ExampleProvider",this);

    @Pact(consumer="JunitRuleMultipleInteractionsConsumer")
    public RequestResponsePact createPact(PactDslWithProvider builder) {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json;charset=UTF-8");

        return builder
                .given("")
                .uponReceiving("Miku")
                .path("/information")
                .query("name=Miku")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body("{\n" +
                        "    \"salary\": 45000,\n" +
                        "    \"name\": \"Hatsune Miku\",\n" +
                        "    \"nationality\": \"Japan\",\n" +
                        "    \"contact\": {\n" +
                        "        \"Email\": \"hatsune.miku@ariman.com\",\n" +
                        "        \"Phone Number\": \"9090950\"\n" +
                        "    }\n" +
                        "}")
                .given("")
                .uponReceiving("Nanoha")
                .path("/information")
                .query("name=Nanoha")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body("{\n" +
                        "    \"salary\": 80000,\n" +
                        "    \"name\": \"Takamachi Nanoha\",\n" +
                        "    \"nationality\": \"Japan\",\n" +
                        "    \"contact\": {\n" +
                        "        \"Email\": \"takamachi.nanoha@ariman.com\",\n" +
                        "        \"Phone Number\": \"9090940\"\n" +
                        "    }\n" +
                        "}")
                .toPact();
    }

    @Test
    @PactVerification()
    public void runTest() {
        providerService.setBackendURL(mockProvider.getUrl());
        Information information = providerService.getInformation();
        assertEquals(information.getName(), "Hatsune Miku");

        providerService.setBackendURL(mockProvider.getUrl(), "Nanoha");
        information = providerService.getInformation();
        assertEquals(information.getName(), "Takamachi Nanoha");
    }
}

This case uses Junit Rule too, but with two interactions in one Pact file.

2.1.3. Junit DSL

PactJunitDSLTest

@RunWith(SpringRunner.class)
@SpringBootTest
public class PactJunitDSLTest {

    @Autowired
    ProviderService providerService;

    private void checkResult(PactVerificationResult result) {
        if (result instanceof PactVerificationResult.Error) {
            throw new RuntimeException(((PactVerificationResult.Error) result).getError());
        }
        assertThat(result, is(instanceOf(PactVerificationResult.Ok.class)));
    }

    @Test
    public void testPact1() {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json;charset=UTF-8");

        RequestResponsePact pact = ConsumerPactBuilder
                .consumer("JunitDSLConsumer1")
                .hasPactWith("ExampleProvider")
                .given("")
                .uponReceiving("Query name is Miku")
                .path("/information")
                .query("name=Miku")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body("{\n" +
                        "    \"salary\": 45000,\n" +
                        "    \"name\": \"Hatsune Miku\",\n" +
                        "    \"nationality\": \"Japan\",\n" +
                        "    \"contact\": {\n" +
                        "        \"Email\": \"hatsune.miku@ariman.com\",\n" +
                        "        \"Phone Number\": \"9090950\"\n" +
                        "    }\n" +
                        "}")
                .toPact();

        MockProviderConfig config = MockProviderConfig.createDefault();
        PactVerificationResult result = runConsumerTest(pact, config, (mockServer, context) -> {
            providerService.setBackendURL(mockServer.getUrl(), "Miku");
            Information information = providerService.getInformation();
            assertEquals(information.getName(), "Hatsune Miku");
            return null;
        });

        checkResult(result);
    }

    @Test
    public void testPact2() {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json;charset=UTF-8");

        RequestResponsePact pact = ConsumerPactBuilder
                .consumer("JunitDSLConsumer2")
                .hasPactWith("ExampleProvider")
                .given("")
                .uponReceiving("Query name is Nanoha")
                .path("/information")
                .query("name=Nanoha")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body("{\n" +
                        "    \"salary\": 80000,\n" +
                        "    \"name\": \"Takamachi Nanoha\",\n" +
                        "    \"nationality\": \"Japan\",\n" +
                        "    \"contact\": {\n" +
                        "        \"Email\": \"takamachi.nanoha@ariman.com\",\n" +
                        "        \"Phone Number\": \"9090940\"\n" +
                        "    }\n" +
                        "}")
                .toPact();

        MockProviderConfig config = MockProviderConfig.createDefault();
        PactVerificationResult result = runConsumerTest(pact, config, (mockServer, context) -> {
            providerService.setBackendURL(mockServer.getUrl(), "Nanoha");
            Information information = providerService.getInformation();
            assertEquals(information.getName(), "Takamachi Nanoha");
            return null;
        });

        checkResult(result);
    }
}

Comparing with Basic Junit and Junit Rule usage, the DSL provides the ability to create multiple Pact files in one test class.

PactJunitDSLJsonBodyTest

@RunWith(SpringRunner.class)
@SpringBootTest
public class PactJunitDSLJsonBodyTest {

    @Autowired
    ProviderService providerService;

    private void checkResult(PactVerificationResult result) {
        if (result instanceof PactVerificationResult.Error) {
            throw new RuntimeException(((PactVerificationResult.Error) result).getError());
        }
        assertThat(result, is(instanceOf(PactVerificationResult.Ok.class)));
    }

    @Test
    public void testWithPactDSLJsonBody() {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json;charset=UTF-8");

        DslPart body = new PactDslJsonBody()
                .numberType("salary", 45000)
                .stringType("name", "Hatsune Miku")
                .stringType("nationality", "Japan")
                .object("contact")
                .stringValue("Email", "hatsune.miku@ariman.com")
                .stringValue("Phone Number", "9090950")
                .closeObject();

        RequestResponsePact pact = ConsumerPactBuilder
                .consumer("JunitDSLJsonBodyConsumer")
                .hasPactWith("ExampleProvider")
                .given("")
                .uponReceiving("Query name is Miku")
                .path("/information")
                .query("name=Miku")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body(body)
                .toPact();

        MockProviderConfig config = MockProviderConfig.createDefault(PactSpecVersion.V3);
        PactVerificationResult result = runConsumerTest(pact, config, (mockServer, context) -> {
            providerService.setBackendURL(mockServer.getUrl());
            Information information = providerService.getInformation();
            assertEquals(information.getName(), "Hatsune Miku");
            return null;
        });

        checkResult(result);
    }

    @Test
    public void testWithLambdaDSLJsonBody() {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json;charset=UTF-8");

        DslPart body = newJsonBody((root) -> {
            root.numberValue("salary", 45000);
            root.stringValue("name", "Hatsune Miku");
            root.stringValue("nationality", "Japan");
            root.object("contact", (contactObject) -> {
                contactObject.stringMatcher("Email", ".*@ariman.com", "hatsune.miku@ariman.com");
                contactObject.stringType("Phone Number", "9090950");
            });
        }).build();

        RequestResponsePact pact = ConsumerPactBuilder
                .consumer("JunitDSLLambdaJsonBodyConsumer")
                .hasPactWith("ExampleProvider")
                .given("")
                .uponReceiving("Query name is Miku")
                .path("/information")
                .query("name=Miku")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body(body)
                .toPact();

        MockProviderConfig config = MockProviderConfig.createDefault(PactSpecVersion.V3);
        PactVerificationResult result = runConsumerTest(pact, config, (mockServer, context) -> {
            providerService.setBackendURL(mockServer.getUrl());
            Information information = providerService.getInformation();
            assertEquals(information.getName(), "Hatsune Miku");
            return null;
        });

        checkResult(result);
    }
}

When use Json Body in DSL usage, we can control the test accuracy by defining whether we check the response body's attributes by Value or by Type, or even with Regular Expression. Comparing with normal Json body, the Lambda statements can avoid using .close**() methods to make the codes more clean. You can find more description about it here.

2.2. Run the Tests at Consumer Miku side

Because we are using Junit, so to run the tests and create Pact files are very easy, just as what we always run our usual Unit Test:

./gradlew :example-consumer-miku:clean test

After that, you can find 7 JSON files created in folder Pacts\Miku. These are the Pacts which contain the contract between Miku and Provider, and these Pacts will be used to drive the Contract Test later at Provider side.

2.3. Publish Pacts to Pact Broker

The JSON files generated with task test are in the local folder which are only reachable to our local Provider, while for real project practice, it's highly recommended to use Pact Broker to transport the Pacts between Consumers and Providers.

There's a docker-compose.yml file that aids setting up an instance of the Pact Broker. Run docker-compose up and you'll have a Broker running at http://localhost/. It'll set up an instance of PostgreSQL as well, but the data will be lost at every restart. In order to use it, in build.gradle, set pactBrokerUrl to http://localhost and both pactBrokerUsername and pactBrokerPassword to ''.

After you set up a Pact Broker server for your own, you can easily share your Pacts to the broker with the pactPublish command:

./gradlew :example-consumer-miku:pactPublish

This command will upload your Pacts to the broker server:

> Task :example-consumer-miku:pactPublish 
Publishing JunitDSLConsumer1-ExampleProvider.json ... HTTP/1.1 200 OK
Publishing JunitDSLJsonBodyConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
Publishing JunitDSLLambdaJsonBodyConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
Publishing BaseConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
Publishing JunitRuleConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
Publishing JunitRuleMultipleInteractionsConsumer-ExampleProvider.json ... HTTP/1.1 200 OK
Publishing JunitDSLConsumer2-ExampleProvider.json ... HTTP/1.1 200 OK

Then you can find the Relationships in our Pact Broker

you can find their are '7' consumers to 1 provider, in real project, it should NOT like that, because we have only one consumer Miku here, it should be only 1 consumer to 1 provider, while in this example, making it's '7' is only to show that how Pact Broker can display the relationships beautifully.

image

Later, our Provider can fetch these Pacts from broker to drive the Contract Test.

2.4. Run the Contract Test at Provider side

We are using the Pact Gradle task in Provider side to run the Contract Test, which can be very easy without writing any code, just execute the pactVerify task. Before we run the test, make sure the Provider API is already started and running at our localhost.

Then we can run the test as:

./gradlew :example-provider:pactVerify

and you can find the test results at the command line, would be something likes:

Arimans-MacBook-Pro:pact-jvm-example ariman$ ./gradlew :example-provider:pactVerify                                          

> Task :example-provider:pactVerify_ExampleProvider 

Verifying a pact between Miku - Base contract and ExampleProvider
  [Using File /Users/ariman/Workspace/Pacting/pact-jvm-example/Pacts/Miku/BaseConsumer-ExampleProvider.json]
  Given 
         WARNING: State Change ignored as there is no stateChange URL
  Consumer Miku
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json;charset=UTF-8" (OK)
      has a matching body (OK)
  Given 
         WARNING: State Change ignored as there is no stateChange URL
  Pact JVM example Pact interaction
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json;charset=UTF-8" (OK)
      has a matching body (OK)
      
  ...
  
  Verifying a pact between JunitRuleMultipleInteractionsConsumer and ExampleProvider
    [from Pact Broker https://ariman.pact.dius.com.au/pacts/provider/ExampleProvider/consumer/JunitRuleMultipleInteractionsConsumer/version/1.0.0]
    Given 
           WARNING: State Change ignored as there is no stateChange URL
    Miku
      returns a response which
        has status code 200 (OK)
        includes headers
          "Content-Type" with value "application/json;charset=UTF-8" (OK)
        has a matching body (OK)
    Given 
           WARNING: State Change ignored as there is no stateChange URL
    Nanoha
      returns a response which
        has status code 200 (OK)
        includes headers
          "Content-Type" with value "application/json;charset=UTF-8" (OK)
        has a matching body (OK)
  

You can find the tests checked the Pacts from both local and remote Pact Broker.

Since this example is open to all Pact learners, means everyone can upload their own Pacts to this Pact Broker, including correct and incorrect pacts, so don't be surprised if your tests run failed with the pacts from this Pact Broker. You can always upload correct pacts to the broker, but just don't rely on it, the pacts in this broker may be cleared at anytime for reset a clean environment.

3. Gradle Configuration

Before we continue to Consumer Nanoha, let's look at the Gradle Configuration first:

project(':example-consumer-miku') {
    ...
    test {
        systemProperties['pact.rootDir'] = "$rootDir/Pacts/Miku"
    }
    
    pact {
            publish {
                pactDirectory = "$rootDir/Pacts/Miku"
                pactBrokerUrl = mybrokerUrl
                pactBrokerUsername = mybrokerUser
                pactBrokerPassword = mybrokerPassword
            }
    }
    ...
}


project(':example-consumer-nanoha') {
    ...
    test {
        systemProperties['pact.rootDir'] = "$rootDir/Pacts/Nanoha"
    }
    ...
}

import java.net.URL

project(':example-provider') {
    ...
    pact {
            serviceProviders {
                ExampleProvider {
                    protocol = 'http'
                    host = 'localhost'
                    port = 8080
                    path = '/'
    
                    // Test Pacts from local Miku
                    hasPactWith('Miku - Base contract') {
                        pactSource = file("$rootDir/Pacts/Miku/BaseConsumer-ExampleProvider.json")
                    }
    
                    hasPactsWith('Miku - All contracts') {
                        pactFileLocation = file("$rootDir/Pacts/Miku")
                    }
    
                    // Test Pacts from Pact Broker
                    hasPactsFromPactBroker(mybrokerUrl, authentication: ['Basic', mybrokerUser, mybrokerPassword])
    
                    // Test Pacts from local Nanoha
    //                hasPactWith('Nanoha - With Nantionality') {
    //                    pactSource = file("$rootDir/Pacts/Nanoha/ConsumerNanohaWithNationality-ExampleProvider.json")
    //                }
    
    //                hasPactWith('Nanoha - No Nantionality') {
    //                    stateChangeUrl = new URL('http://localhost:8080/pactStateChange')
    //                    pactSource = file("$rootDir/Pacts/Nanoha/ConsumerNanohaNoNationality-ExampleProvider.json")
    //                }
                }
            }
        }
    }

Here we have configuration separately for Consumer Nanoha, Consumer Miku and Provider.

  • systemProperties['pact.rootDir'] defines the path where consumer Miku and Nanoha generate their Pacts files locally.

  • pact { ... } defines the the Pact Broker URL and where to fetch pacts to upload to the broker.

  • Then in Provider configuration, hasPactWith() and hasPactsWith() specify the local path to find Pact files, and hasPactsFromPactBroker specify the remote Pact Broker to fetch Pact files.

Why comment Nanoha's Pacts path? Because we haven't created Nanoha's Pacts, so it will raise exception with that configuration, we can uncomment that path later after we created the Nanoha's Pacts files.

4. Contract Test between Provider and Consumer Nanoha

The Contract Test between Nanoha and Provider is similar to Miku, the difference I want to demonstrate here is Provider State, you can find more detailed explanation about the Provider State here.

4.1. Preparation for Provider State

In our example Provider, the returned payload for both Miku and Nanoha have a property .nationality, this kind of properties values, in real project, could always be queried from database or dependency service, but in this example, to simplify the Provider application logic, I just create a static property to simulate the data storage:

provider.ulti.Nationality

public class Nationality {
    private static String nationality = "Japan";

    public static String getNationality() {
        return nationality;
    }

    public static void setNationality(String nationality) {
        Nationality.nationality = nationality;
    }
}

Then we can change the .nationality value at anytime to achieve our test purpose. To change its value, I created a PactController which can accept and reset the state value by sending a POST to endpoint /pactStateChange:

provider.PactController

@Profile("pact")
@RestController
public class PactController {

    @RequestMapping(value = "/pactStateChange", method = RequestMethod.POST)
    public PactStateChangeResponseDTO providerState(@RequestBody PactState body) {
        switch (body.getState()) {
            case "No nationality":
                Nationality.setNationality(null);
                System.out.println("Pact State Change >> remove nationality ...");
                break;
            case "Default nationality":
                Nationality.setNationality("Japan");
                System.out.println("Pact Sate Change >> set default nationality ...");
                break;
        }

        // This response is not mandatory for Pact state change. The only reason is the current Pact-JVM v4.0.4 does
        // check the stateChange request's response, more exactly, checking the response's Content-Type, couldn't be
        // null, so it MUST return something here.
        PactStateChangeResponseDTO pactStateChangeResponse = new PactStateChangeResponseDTO();
        pactStateChangeResponse.setState(body.getState());

        return pactStateChangeResponse;
    }
}

Because this controller should only be available at Non-Production environment, so I added a Profile annotation to limit that this endpoint is only existing when Provider application is running with a "pact" profile, in another words, it only works in test environment.

So, to summary all above things, when Provider is running with a "pact" profile, it serves an endpoint /pactStateChange which accept a POST request to set the .nationality value to be "Japan" (by default) or "null".

4.2. Create Test Case at Consumer Nanoha side

The test cases at Nanoha side is using DSL Lambda, here we have two cases in our test, the main difference between them is the expectation for .nationality and the Provider State we set in .given().

@RunWith(SpringRunner.class)
@SpringBootTest
public class NationalityPactTest {

    @Autowired
    ProviderService providerService;

    private void checkResult(PactVerificationResult result) {
        if (result instanceof PactVerificationResult.Error) {
            throw new RuntimeException(((PactVerificationResult.Error) result).getError());
        }
        assertThat(result, is(instanceOf(PactVerificationResult.Ok.class)));
    }

    @Test
    public void testWithNationality() {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json;charset=UTF-8");

        DslPart body = newJsonBody((root) -> {
            root.numberType("salary");
            root.stringValue("name", "Takamachi Nanoha");
            root.stringValue("nationality", "Japan");
            root.object("contact", (contactObject) -> {
                contactObject.stringMatcher("Email", ".*@ariman.com", "takamachi.nanoha@ariman.com");
                contactObject.stringType("Phone Number", "9090940");
            });
        }).build();

        RequestResponsePact pact = ConsumerPactBuilder
                .consumer("ConsumerNanohaWithNationality")
                .hasPactWith("ExampleProvider")
                .given("")
                .uponReceiving("Query name is Nanoha")
                .path("/information")
                .query("name=Nanoha")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body(body)
                .toPact();

        MockProviderConfig config = MockProviderConfig.createDefault(PactSpecVersion.V3);
        PactVerificationResult result = runConsumerTest(pact, config, (mockServer, context) -> {
            providerService.setBackendURL(mockServer.getUrl());
            Information information = providerService.getInformation();
            assertEquals(information.getName(), "Takamachi Nanoha");
            assertEquals(information.getNationality(), "Japan");
            return null;
        });

        checkResult(result);
    }

    @Test
    public void testNoNationality() {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json;charset=UTF-8");

        DslPart body = newJsonBody((root) -> {
            root.numberType("salary");
            root.stringValue("name", "Takamachi Nanoha");
            root.stringValue("nationality", null);
            root.object("contact", (contactObject) -> {
                contactObject.stringMatcher("Email", ".*@ariman.com", "takamachi.nanoha@ariman.com");
                contactObject.stringType("Phone Number", "9090940");
            });
        }).build();

        RequestResponsePact pact = ConsumerPactBuilder
                .consumer("ConsumerNanohaNoNationality")
                .hasPactWith("ExampleProvider")
                .given("No nationality")
                .uponReceiving("Query name is Nanoha")
                .path("/information")
                .query("name=Nanoha")
                .method("GET")
                .willRespondWith()
                .headers(headers)
                .status(200)
                .body(body)
                .toPact();

        MockProviderConfig config = MockProviderConfig.createDefault(PactSpecVersion.V3);
        PactVerificationResult result = runConsumerTest(pact, config, (mockServer, context) -> {
            providerService.setBackendURL(mockServer.getUrl());
            Information information = providerService.getInformation();
            assertEquals(information.getName(), "Takamachi Nanoha");
            assertNull(information.getNationality());
            return null;
        });

        checkResult(result);
    }
}

To run the test:

./gradlew :example-consumer-nanoha:clean test

Then you can find two Pacts files generated at Pacts\Nanoha.

4.3. Run Contract Test at Provider side

4.3.1. start Provider application

As we mentioned above, before we running our test, we need start Provider application with profile "pact" (if your Provider application is already started when you ran test with Miku, please kill it first), the command is:

SPRING_PROFILES_ACTIVE=pact ./gradlew :example-provider:bootRun

4.3.2. update Gradle configuration

it's time to uncomment the Pacts file searching path in Provider configuration:

build.gralde

    hasPactWith('Nanoha - With Nantionality') {
        pactSource = file("$rootDir/Pacts/Nanoha/ConsumerNanohaWithNationality-ExampleProvider.json")
    }

    hasPactWith('Nanoha - No Nantionality') {
        stateChangeUrl = new URL('http://localhost:8080/pactStateChange')
        pactSource = file("$rootDir/Pacts/Nanoha/ConsumerNanohaNoNationality-ExampleProvider.json")
    }

The first Pact will check the payload with default .nationality which value is Japan, while the second Pact, we define a stateChangeUrl which is implemented by PactController in Provider as we talked above, and the POST body is defined in .given() in the Pact.

4.3.3. run the contract test

the same command as we did with Consumer Miku:

./gradlew :example-provider:pactVerify

Then you can get the test log in the same terminal.

5. Break Something

Is everything done? Yes, if you followed all above introductions until here, you should be able to create your Contract Test by yourself. But before you go to real, let's try to break something to see how Pact could find the defect.

5.1. Break something in Provider

Both Miku and Nanoha consume Provider's response for a property .name, in real project, there might be a case that Provider would like to change this property name to .fullname, this could be a classic Contract Breaking which would be captured by Pact.

To do that, we need modify several lines of codes in Provider which could be a little difficult to understand, especially for a some testers who are not familiar to Spring Boot application.

So to make the breaking simple, let's just force Provider returns null as the value to .name to all consumers, this can be easily done by adding only a single line:

provider.InformationController

@RestController
public class InformationController {
        ...
        
        information.setName(null);
        return information;
    }
}

5.2. Retest

Restart your Provider application and run the test again, you can get the test failures as this:

...

Verifying a pact between Nanoha - With Nantionality and ExampleProvider
  [Using File /Users/Biao/Workspace/Pacting/Pact-JVM-Example/Pacts/Nanoha/ConsumerNanohaWithNationality-ExampleProvider.json]
  Given
         WARNING: State Change ignored as there is no stateChange URL
  Query name is Nanoha
    returns a response which
      has status code 200 (OK)
      has a matching body (FAILED)

Failures:

0) Verifying a pact between Nanoha - With Nantionality and ExampleProvider - Query name is Nanoha  Given  returns a response which has a matching body
      Verifying a pact between Nanoha - With Nantionality and ExampleProvider - Query name is Nanoha  Given  returns a response which has a matching body=BodyComparisonResult(mismatches={$.nationality=[BodyMismatch(expected="Japan", actual=null, mismatch=Expected "Japan" but received null, path=$.nationality, diff=null)], $.name=[BodyMismatch(expected="Takamachi Nanoha", actual=null, mismatch=Expected "Takamachi Nanoha" but received null, path=$.name, diff=null)]}, diff=[{, -  "nationality": "Japan",, +  "salary": 80000,, +  "name": null,, +  "nationality": null,,   "contact": {,     "Phone Number": "9090940", -  },, -  "name": "Takamachi Nanoha",, -  "salary": 294875537, +  }, }])

...

6. Contribution

I'm not always tracking this example repository with the latest Pact-JVM version, so if you find there is new Pact-JVM released, and you'd like to put it into this example, any PR is welcome.