Testing a Spring Boot REST API Consumer against a Contract with Spring Cloud Contract

Table Of Contents

Consumer-driven contract tests are a technique to test integration points between API providers and API consumers without the hassle of end-to-end tests (read it up in a recent blog post). A common use case for consumer-driven contract tests is testing interfaces between services in a microservice architecture. In the Java ecosystem, Spring Boot is a widely used technology for implementing microservices. Spring Cloud Contract is a framework that facilitates consumer-driven contract tests. So let’s have a look at how to verify a Spring Boot REST client against a contract with Spring Cloud Contract.

Example Code

This article is accompanied by a working code example on GitHub.

In this Article

Instead of testing API consumer and provider in an end-to-end manner, with consumer-driven contract tests we split up the test of our API into two parts:

  • a consumer test testing against a mock provider and
  • a provider test testing against a mock consumer

This article focuses on the consumer side.

In this article we will:

  • define an API contract with Spring Cloud Contract’s DSL
  • create a client against that API with Feign
  • publish the contract to the API provider
  • generate a provider stub against which we can verify our consumer code
  • verify the consumer against the stub locally
  • verify the consumer against the stub online

Define the Contract

With Spring Cloud Contract, contracts are defined with a Groovy DSL:

package userservice

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  description("When a POST request with a User is made, the created user's ID is returned")
  request {
    method 'POST'
    url '/user-service/users'
    body(
      firstName: "Arthur",
      lastName: "Dent"
    )
    headers {
      contentType(applicationJson())
    }
  }
  response {
    status 201
    body(
      id: 42
    )
    headers {
      contentType(applicationJson())
    }
  }
}

The above contract defines an HTTP POST request to /user-service/users with a user object as body that is supposed to save that user to the database and should be answered with HTTP status 201 and the id of the newly created user.

We’ll store the contract in a file called shouldSaveUser.groovy for later usage.

The details of the DSL can be looked up in the Spring Cloud Contract Reference.

Create a Client against the API

We choose Feign as the technology to create a client against the API defined in the contract.

We need to add the Feign dependency to the Gradle build:

dependencies {
    compile("org.springframework.cloud:spring-cloud-starter-openfeign:2.0.1.RELEASE")
    // ... other dependencies
}

Next, we create the actual client and the data classes used in the API:

@FeignClient(name = "userservice")
public interface UserClient {

  @RequestMapping(method = RequestMethod.POST, path = "/user-service/users")
  IdObject createUser(@RequestBody User user);
}
public class User {
	private Long id;
	private String firstName;
	private String lastName;
	// getters / setters / constructors omitted
}
public class IdObject {
	private long id;
	// getters / setters / constructors omitted
}

The @FeignClient annotation tells Spring Boot to create an implementation of the UserClient interface that should run against the host that configured under the name userservice. The @RequestMapping and @RequestBody annotations specify the details of the POST request and the corresponding response defined in the contract.

Publish the Contract to the Provider

The next thing we - as the API consumer - want to do, is to verify that our client code works exactly as the contract specifies. For this verification, Spring Cloud Contracts provides a Stub Runner that takes a contract as input and provides a runtime stub against which we can run our consumer code.

That stub is created via the Spring Cloud Contract Gradle plugin on the provider side. Thus, we need to make the contract available to the provider.

So, we simply clone the provider codebase and put the contract into the file src/test/resources/contracts/userservice/shouldSaveUser.groovy in the provider codebase and push it as a pull request for the provider team to take up.

Note that although we’re still acting as the consumer of the API, in this step and the next, we’re editing the provider’s codebase!

Generate a Provider Stub

Next, we want to generate the stub against which we can verify our consumer code. For this, the Spring Cloud Contract Verifier Gradle plugin has to be set up in the provider build. You can read up on this setup in this article about the provider side.

Additionally to the setup from the article above, in order to publish the stub into a Maven repository, we need to add the maven-publish plugin to the build.gradle:

apply plugin: 'maven-publish'

We want to control the groupId, version and artifactId of the stub so that we can later use these coordinates to load the stub from the Maven repository. For this, we add some information to build.gradle:

group = 'io.reflectoring'
version = '1.0.0'

The artifactId can be set up in settings.gradle (unless you’re OK with it being the name of the project directory, which is the default):

rootProject.name = 'user-service'

Then, we run ./gradlew publishToMavenLocal which should create and publish the artifact io.reflectoring:user-service:1.0.0-stubs to the local Maven repository on our machine. If you’re interested what this artifact looks like, look into the file build/libs/user-service-1.0.0-stubs.jar. Basically, it contains a JSON representation of the contract that can be used as input for a stub that can act as the API provider.

Verify the Consumer Code Locally

After the trip to the provider’s code base, let’s get back to our own code base (i.e. the consumer code base). Now, that we have the stub in our local Maven repository, we can use the Stub Runner to verify that our consumer code works as the contract expects.

For this, we need to add the Stub Runner as a dependency to the Gradle build:

dependencies {
    testCompile("org.springframework.cloud:spring-cloud-starter-contract-stub-runner:2.0.1.RELEASE")
    // ... other dependencies
}

With the Stub Runner in place, we create an integration test for our consumer code:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureStubRunner(
    ids = "io.reflectoring:user-service:+:stubs:6565", 
    stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class UserClientTest {

  @Autowired
  private UserClient userClient;

  @Test
  public void createUserCompliesToContract() {
    User user = new User();
    user.setFirstName("Arthur");
    user.setLastName("Dent");
    IdObject id = userClient.createUser(user);
    assertThat(id.getId()).isEqualTo(42L);
  }

}

With the @AutoConfigureStubRunner annotation we tell the Stub Runner to load the Maven artifact with

  • the groupId io.reflectoring,
  • the artifactId user-service,
  • of the newest version (+) and
  • with the stubs qualifier

from a Maven repository, extract the contract from it and pass it into the Stub Runner who then acts as the API provider on port 6565.

The stubsMode is set to LOCAL meaning that the artifact should be resolved against the local Maven repository on our machine for now. And since we have published the stub to our local Maven repository, it should resolve just fine.

When running the test, you may run into the following exception:

com.netflix.client.ClientException: Load balancer does not have available server for client: userservice

This is because we need to tell the Stub Runner which Maven artifact it is supposed to be used as a stub for which service. Since our Feign client runs against the service named userservice and our artifact has the artifactId user-service (with “-"), we need to add the following config to our application.yml:

stubrunner:
  idsToServiceIds:
    user-service: userservice

Verify the Consumer Code Online

Having verified the consumer code against a stub in our local Maven repository is well and good, but once we push the consumer code to the CI, the build will fail because the stub is not available in an online Maven repository.

Thus, we have to wait until the provider team is finished with implementing the contract and the provider code is pushed to the CI. The provider build pipeline should be configured to automatically publish the stub to an online Maven repository like a Nexus or Artifactory installation.

Once the provider build has passed the CI build pipeline, we can adapt our test and set the stubsMode to REMOTE so that the stub will be loaded from our Nexus or Artifactory server:

@AutoConfigureStubRunner(
  ids = "io.reflectoring:user-service:+:stubs:6565",
  stubsMode = StubRunnerProperties.StubsMode.REMOTE)
public class UserClientTest {
  //...
}

In order for the Stub Runner to find the online Maven repository, we need to tell it where to look in the application.yml:

stubrunner:
  repositoryRoot: http://path.to.repo/repo-name

Now, we can push the consumer code and be certain that the consumer and provider are compatible to each other.

Conclusion

This article gave a quick tour of the consumer-side workflow of Spring Cloud Contract. We created a Feign client and verified it against a provider stub which is created from a contract. The workflow requires good communication between the consumer and provider teams, but that is the nature of integration tests. Once the workflow is understood by all team members, it lets us sleep well at night since it protects us from syntactical API issues between consumer and provider.

Written By:

Tom Hombergs

Written By:

Tom Hombergs

As a professional software engineer, consultant, architect, general problem solver, I've been practicing the software craft for more than fifteen years and I'm still learning something new every day. I love sharing the things I learned, so you (and future me) can get a head start. That's why I founded reflectoring.io.

Recent Posts

Guide to JUnit 5 Functional Interfaces

In this article, we will get familiar with JUnit 5 functional interfaces. JUnit 5 significantly advanced from its predecessors. Features like functional interfaces can greatly simplify our work once we grasp their functionality.

Read more

Getting Started with Spring Security and JWT

Spring Security provides a comprehensive set of security features for Java applications, covering authentication, authorization, session management, and protection against common security threats such as CSRF (Cross-Site Request Forgery).

Read more

Creating and Publishing an NPM Package with Automated Versioning and Deployment

In this step-by-step guide, we’ll create, publish, and manage an NPM package using TypeScript for better code readability and scalability. We’ll write test cases with Jest and automate our NPM package versioning and publishing process using Changesets and GitHub Actions.

Read more