I introduced the contract test concept in the previous blog. In this blog we will explore writing contract tests for services which communicate over Http. Tests are written using:

  1. Pact-jvm
  2. Junit5
  3. Gradle

The code is available on github.

Consumers drive contracts. They create and pass it to provider. Provider is supposed to make sure the consumer expectations are met. Provider cannot roll out it’s change if consumer’s expectation are not met. Pact will help us test it.

Consumer test

Consumer tests are generally written for a class which are responsible for communicating with other service. Let’s start with the first test. Intent of this test is to cover the interaction between loan gateway and fraud service. Loan gateway and fraud service are the components which I introduced in the previous blog. Loan gateway passes customer id to fraud service and expects a response containing whether the given customer has fraudulent status or not. Following is a code snippet of FraudCheckService in the loan gateway service.

Let’s take a step by step approach to build the contract tests.

  1. Write a spring boot test which asserts that for customer id “1” the fraud status is false.

  2. This test is going to fail as there is no fraud service available which is going to respond. Test passes after setting up a mock fraud service.

  3. Let’s define a method which defines the contract.

    This method is self-explanatory. We basically describe what the request and response are. Let’s break it down.

    @Pact(provider = "fraud_service", consumer = "loan_gateway")Provider and consumer names, these will be published along with the contract
    given("Customer with id 1 is setup to return false fraudulent status")This is optional, it enables the provider to do test setup. We will cover this more in provider test
    uponReceiving("When fraud status is requested for nonFraudulent customer")This is the interaction description, basically it covers the scenario.
    method("GET")Http method
    path("/fraud/1")Uri
    status(200)Response status code
    body(JSONObject("""{ "status": false }"""))Response body

  4. Annotate test method with PactTestFor annotation. By this a test method connects to the pact method.
  5. Annotate test class with PactTestFor annotation. Extend the test with PactConsumerTestExt class.
  6. Run the test, it will fail with java.net.BindException: Address already in use exception. This is because Pact itself starts the mock server. Let’s remove the mock server. Test looks like below.
  7. Run the test. It passes now. As a nice side effect, it generates the contract at /target/pacts folder.

  8. This contract is shared with provider and provider makes sure that it adheres to the consumer expectation. For easy sharing, pact provides a broker which stores the published contracts. We will cover broker topic separately.

Provider test

Provider tests are written for the controller layer. Rest of the service components are stubbed/mocked. This is because the interaction between controller and other components is already tested with the integration tests.

Let’s start with the provider test.

  1. Write a spring boot test for the provider service controller. In our case it is FraudServiceController. Let’s skip the other components.

  2. Let’s convert the spring boot test into a provider test. We set up the context with host and port. And verify the interactions.

  3. Right now pact does not know about from where to pick up the pact and which interactions to test from those pact files. Let’s supply that information via annotations.

  4. Run the test. Test fails with an exception au.com.dius.pact.provider.junit.MissingStateChangeMethod: Did not find a test class method annotated with @State("Customer with id 1 is setup to return false fraudulent status")
  5. Looking at the string inside state annotation makes us realise that we have mentioned this string in contract under given clause. Let’s recall the purpose. It’s optional, however it enables the provider to do test setup. This information is captured under providerStates node in the contract json.
  6. Let’s add a setup method, ideally this is not required as the setup is already taken care of by the mock. However as it is present in the contract it is now mandatory to define it. Consumers can skip it however I recommend to keep it as consumers may not know what data provider needs to setup. For now let’s add an empty method as below.

  7. The entire test looks like below.

  8. Run the test. Now it will pass and will emit the output as below. Last line in the output suggests that the verification results are not published even though the test is passed.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    Verifying a pact between loan_gateway and fraud_service
      [Using Directory target/pacts]
      Given Customer with id 1 is setup to return false fraudulent status
      When fraud status is requested for nonFraudulent customer
      ...
        returns a response which
          has status code 200 (OK)
          has a matching body (OK)
    ... Skipping publishing of verification results as it has been disabled (pact.verifier.publishResults is not 'true')

Publish verification results

  1. Pact looks for an environment variable pact.verifier.publishResults and if it is set to true then the results are published.
  2. One can set this property in the setup method of the provider tests. However this needs to be repeated for each provider test class.
  3. Alternatively we can achieve this by implementing a BeforeAllCallback hook where we would get a handle to set this property.

  4. Extend the provider test with PactVerificationResultExtension as below

  5. That’s it, now the verification results would be published.
  6. Problem with this approach is, we hard coded the provider version. Now let’s take one more step and use project version specified in the build.gradle.

Refer application version from build.gradle

  1. Let’s use Gradle’s ProcessResources task. It enables us to access gradle properties in the application properties file. Add below code to build.gradle.

  2. Access the project version in application.yaml file as below.

  3. Access app.version in the PactVerificationResultExtension file and set it to the pact.provider.version

That’s it for now. In the next blog we will explore writing contract tests between services which communicates asynchronously.