pact merging flow A few months ago our frontend team encountered difficulties during work with client’s remote backend team. Two groups of people which cannot directly communicate with each other and frequent API changes led to downturn of project’s development.

The API updates were frequent, and because of lack of communication we were not aware of them until performing checks of the whole business flow.

We’ve lost trust to the API and noticed that we should’ve started the development with something that would ensure us that both producer and consumer sides use the same API.

The question was: How do we know that we implement the same API? which led us to another question: Can we be sure that our test mocks behave like the real REST API?.

We decided to dig into it and try to do something to be sure that we have a comprehensive answers to these questions.


Not a solution

Commonly End-to-End tests (E2E) are considered as tests which side effect is that we know that the APIs on the frontend and backend side are not consistent. The problem is they don’t strictly say that the problem is API inconsistency. Their main task is to check the flow of the usage, not the API itself. They also take time. It’s hassle to set up the whole context and run E2E tests to check if the API is implemented properly.

The one to rule them all

Contract testing is a way to ensure that services communicate with each other with the same API “language”. It’s based on the contract (prepared by the producer or consumer) which firms that both sides implement the same API. Both frontend and backend tests are based on the same contract. Contract testing is the killer of the API version hell.

Backend tests don’t need a whole context of the application set up. They only require endpoints and the stubs of injected services methods.

On the other hand frontend tests are just unit tests with stubs of the backend service endpoints.

I’m gonna make him an offer he can’t refuse

How does look consumer driven contracts development? The frontend and backend developer create agreement how the API should look like in the form of the contract file. The basic idea is the contract is being written as a part of consumer tests. It’s worth noting that contract defines minimal set of request/response fields that should be present during the communication so if you add new fields they won’t break the contract.

PACT

PACT aids developers to achieve this. The major advantage is it’s well supported by Angular, and it’s contract files, which are contract base for generating producer tests, can be shared with JVM application.

Sharing pacts between consumers and producer

PACT provides a solution to store the contract files called PACT Broker. It is a repository for publishing and retrieving pacts with REST API.

Unfortunately we had no time to work with another new repository, so we decided to prepare a simple flow on Jenkins and share pacts over git repository instead of running broker.

The following diagram shows our concept of sharing pacts without the broker:

pact merging flow

Spring Cloud Contract

Spring Cloud Contract is a set of tools that supports consumer-driven contracts in Spring applications. Project is focused on the custom DSL solution, fortunately they also provide support for PACTs, so we are able to work together with non-JVM consumer like Angular frontend application flawlessly.

Test generation

Our task was to implement the API for sending simple message and returning the id of the message. The given contract looks as below:

{
  "consumer": {
    "name": "frontend-app"
  },
  "provider": {
    "name": "backend-app"
  },
  "interactions": [
    {
      "description": "POST new message",
      "providerState": "provider accepts message",
      "request": {
        "method": "POST",
        "path": "/message",
        "headers": {
          "Content-Type": "application/json;charset=UTF-8"
        },
        "body": {
          "message": "Sample message"
        }
      },
      "response": {
        "status": 201,
        "headers": {
          "Content-Type": "application/json;charset=UTF-8"
        },
        "body": {
          "id": "25e3ae11-d294-4a69-9421-2816df07b531"
        },
        "matchingRules": {
          "$.body": {
            "match": "type"
          }
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "2.0.0"
    }
  }
}

Let’s assume that generated pact is already in the producer’s repository src/test/resources/pacts/messaging.json

Preparing build.gradle

We need to add dependencies and enable a gradle plugin:

buildscript {
    ...
    dependencies {
        ...
        classpath("org.springframework.cloud:spring-cloud-contract-gradle-plugin:${springCloudContractVersion}")
        classpath("org.springframework.cloud:spring-cloud-contract-pact:${springCloudContractVersion}")
        ...
    }
}

...
apply plugin: 'spring-cloud-contract'
...

dependencies {
    ...
    testImplementation("au.com.dius:pact-jvm-provider-spring_2.12:${pactVersion}") // we need this one to use PACT provider annotations
    testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier:${springCloudContractVersion}") // tests use the SpringCloudContractAssertions 
    ...
}

After that, we need to configure contracts section in gradle which is used to generate tests:

import org.springframework.cloud.contract.verifier.config.TestFramework

contracts {
    targetFramework = TestFramework.JUNIT // it's default value, you can use SPOCK instead
    contractsPath = "pacts/" // default directory is `contracts/`
    baseClassForTests = 'com.inspeerity.article.contract.MessagingContractMocks' // here we specify which class will be extended by the generated tests
    basePackageForTests = 'com.inspeerity.article.contract' // base package for generated tests
}

Base class for tests

We need to implement base class for tests. It stores information about test definitions we want to run and defines service method stubs.

Let’s create MessagingContractMocks as we defined it as baseClassForTests:

package com.inspeerity.article.contract;

import ...

@RunWith(SpringRestPactRunner.class)
@WebMvcTest(MessageController.class)
@PactFolder("contracts/")
@Provider("backend-app")
abstract class MessagingContractMocks {

    @TestTarget
    public final MockMvcTarget target = new MockMvcTarget();
    
    @Autowired
    private MessageControler messageControler;
    
    @Before
    public void setupBefore() {
        MockitoAnnotations.initMocks(this);
        target.setControllers(featureController);
    }
    
    @MockBean
    private MessageSender messageSender;
    
    @State("provider accepts message")
    public void aRequestToGETFeatures() {
    }
}

You may notice that:

  • We use dedicated pact spring library, so we can use SpringRestPactRunner instead of PactRunner as the Junit’s runner. It allows us to use spring test annotations.
  • We’ve put @WebMvcTest annotation because for contract testing purposes all we need in the application context are web related components and we’ll mock beans from other layers.
  • @Provider annotation defines we are interested in contracts backend-app provider.
  • If you have more than one consumer or use PACT Proker you may also need to define @Consumer annotation to define which contract you want to use.
  • Contract tests require @TestTarget annotated Target interface implementation, which implementation should throw an exception on unexpected response. MockMvcTarget is out-of-the-box implementation that verifies controller responses. It must be defined if you use SpringRestPactRunner and all tested controllers need to be set, otherwise they won’t be checked and test will fail with Not found error.
  • We use @MockBean annotation to create MessageSender bean mock. It’s used by our controller so we will need to stub it’s method later.
  • We define @State annotated method. It’s our place to define our mocks and stubs for the chosen contract state.

Generating test class

To check if test generates test class run:

./gradlew generateContractTests

Here is your generated test class!

generated test class

Contract tests are based on response types, not particular values (they don’t test the logic). That’s why the generated tests check value types instead of values.

Try to run your tests:

./gradlew test

As you see the test fails:

test failed

Description explains with details that the:

  1. Content-Type header is missing
  2. status is 404 instead expected 201
  3. response type is test/plain instead expected application/json

Implementation

We need to create MessageController:

package com.inspeerity.article.contract.messaging;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessageControler {

    private final MessageSender messageSender;

    @Autowired
    public MessageController(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    @PostMapping(
            consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE
    )
    @ResponseStatus(HttpStatus.CREATED)
    public MessageDto postMessage(@RequestBody MessageCommand messageCommand) {
        return new MessageDto(messageSender.sendMessage(messageCommand.getMessage()));
    }
}

Of course you need to create also classes:

  • input data type: MessageCommand with field message of String type
  • MessageDto with one field id of UUID type
  • MessageSender annotated with @Service with public method sendMessage(String message)returning UUID

You don’t need to create any logic in MessageSender just return some UUID. In fact for the tutorial purposes we will just care about it’s method stub.

Do not forget to stub the service method:

@State("provider accepts message")
public void postNewMessage() {
    when(messageSender.sendMessage("Sample message")) // we need to stub service method we used in controller
            .thenReturn(UUID.fromString("25e3ae11-d294-4a69-9421-2816df07b531")); // this data comes from pact file
}

Now if you run:

./gradlew test

You will see the test passed:

test ok

What have we gained?

Our problem was the API inconsistency. By using contract tests we get rid of the hassle and time consuming E2E tests. Important thing is the backend developers must be aware that if they break the contract by i.e. changing the type of a field the test will fail. On the other hand if frontend developers need a new endpoint or modify the old data format, they need to prepare a new contract.

Updated:

Leave a comment