Testing a Spring Boot REST API 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 test a REST API provided by a Spring Boot application against a contract previously defined by the API consumer using 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 provider side. A consumer of our API has created a contract in advance and we want to verify that the REST API provided by our Spring Boot Service matches the expectations of that contract.

In this article we will:

  • have a look at the API contract created in advance by an API consumer
  • create a Spring MVC controller providing the desired REST API
  • set up Spring Cloud Contract to automatically generate JUnit tests that verify the controller against the contract

The Contract

In Spring Cloud Contract contracts are defined with a DSL in a Groovy file. The contract we’re using in this article looks like this:

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())
    }
  }
}

Each contract defines a single request / response pair. The contract above defines an API provided by user-service that consists of a POST request to the URL /user-service/users containing some user data in the body and an expected response to that request returning HTTP code 201 and the newly created user’s database id as body.

For later usage, the contract file is expected to be filed under src/test/resources/contracts/userservice/shouldSaveUser.groovy.

The Spring Controller

A Spring controller that obeys the above contract is easily created:

@RestController
public class UserController {

  private UserRepository userRepository;

  @Autowired
  public UserController(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  @PostMapping(path = "/user-service/users")
  public ResponseEntity<IdObject> createUser(@RequestBody @Valid User user) {
    User savedUser = this.userRepository.save(user);
    return ResponseEntity
      .status(201)
      .body(new IdObject(savedUser.getId()));
  }

}

IdObject is a simple bean that has the single field id.

The Provider Test

Next, let’s set up Spring Cloud Contract to verify that the above controller really obeys the contract. We’re going to use Gradle as build tool (but Maven is supported as well).

Test Base

To verify an API provider (the Spring controller in our case), Spring Cloud Contract automatically generates JUnit tests from a given contract. In order to give these automatically generated tests a working context, we need to create a base test class which is subclassed by all generated tests:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public abstract class UserServiceBase {

  @Autowired
  WebApplicationContext webApplicationContext;

  @MockBean
  private UserRepository userRepository;

  @Before
  public void setup() {
    User savedUser = new User();
    savedUser.setFirstName("Arthur");
    savedUser.setLastName("Dent");
    savedUser.setId(42L);
    when(userRepository.save(any(User.class))).thenReturn(savedUser);
    RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
  }
}

In this base class, we’re setting up a Spring Boot application with @SpringBootTest and are mocking away the UserRepository so that it always returns the user specified in the contract. Then, we set up RestAssured so that the generated tests can simply use RestAssured to send requests against our controller.

Note that the contract DSL allows to specify matchers instead of static content, so that the user name defined in our contract does not have to be “Arthur Dent” but may for example be any String.

Setting up the build.gradle

Spring Cloud Contract provides a Gradle plugin that takes care of generating the tests for us:

apply plugin: 'spring-cloud-contract'

The plugin needs the following dependencies withing the buildscript scope:

buildscript {
  repositories {
    // ...
  }
  dependencies {
    classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.4.RELEASE"
    classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:2.0.1.RELEASE"
  }
}

In the contracts closure, we define some configuration for the plugin:

contracts {
  baseClassMappings {
    baseClassMapping(".*userservice.*", "io.reflectoring.UserServiceBase")
  }
}

The mapping we defined above tells Spring Cloud Contract that the tests generated for any contracts it finds in src/test/resources/contracts that contain “userservice” in their path are to be subclassed from our test base class UserServiceBase. We could define more mappings if different tests require different setups (i.e. different base classes).

In order for the automatically generated tests to work, we need to include some further dependencies in the testCompile scope:

dependencies {
  // ...
  testCompile('org.codehaus.groovy:groovy-all:2.4.6')
  testCompile("org.springframework.cloud:spring-cloud-starter-contract-verifier:2.0.1.RELEASE")
  testCompile("org.springframework.cloud:spring-cloud-contract-spec:2.0.1.RELEASE")
  testCompile("org.springframework.boot:spring-boot-starter-test:2.0.4.RELEASE")
}

The Generated Test

Once we call ./gradlew generateContractTests, the Spring Cloud Contract Gradle plugin will now generate a JUnit test in the folder build/generated-test-sources:

public class UserserviceTest extends UserServiceBase {

   @Test
   public void validate_shouldSaveUser() throws Exception {
      // given:
         MockMvcRequestSpecification request = given()
               .header("Content-Type", "application/json")
               .body("{\"firstName\":\"Arthur\",\"lastName\":\"Dent\"}");

      // when:
         ResponseOptions response = given().spec(request)
               .post("/user-service/users");

      // then:
         assertThat(response.statusCode()).isEqualTo(201);
         assertThat(response.header("Content-Type")).matches("application/json.*");
      // and:
         DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
         assertThatJson(parsedJson).field("['id']").isEqualTo(42);
   }
   
}

As you can see, the generated test sends the request specified in the contract an validates that the controller returns the response expected from the contract.

The Gradle task generateContractTests is automatically included within the build task so that a normal build will generate and then run the tests.

Bonus: Generating Tests from a Pact

Above, we used a contract defined with the Spring Cloud Contract DSL. However, Spring Cloud Contract currently only supports JVM languages and you might want to verify a contract generated by a non-JVM consumer like an Angular application. In this case you may want to use Pact on the consumer side since Pact supports other languages as well. You can read up how to create a contract with Pact from an Angular client in this article.

Spring Cloud Contract Pact Support

Luckily, Spring Cloud Contract supports the Pact contract format as well. To automatically generate tests from a pact file, you need to put the pact file (which is a JSON file) into the folder src/test/contracts and add these dependencies to your build.gradle:

buildscript {
    repositories {
      // ...
    }
    dependencies {
        // other dependencies ...
        classpath "org.springframework.cloud:spring-cloud-contract-spec-pact:1.2.5.RELEASE"
        classpath 'au.com.dius:pact-jvm-model:2.4.18'
    }
}

Spring Cloud Contract then automatically picks up the pact file and generates tests for it just like for the “normal” contract files.

Conclusion

In this article, we set up a Gradle build using Spring Cloud Contract to auto-generate tests that verify that a Spring REST controller obeys a certain contract. Details about Spring Cloud Contract can be looked up in the reference manual. Also, check the github repo containing the example code to this article.

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

Inheritance, Polymorphism, and Encapsulation in Kotlin

In the realm of object-oriented programming (OOP), Kotlin stands out as an expressive language that seamlessly integrates modern features with a concise syntax.

Read more

Publisher-Subscriber Pattern Using AWS SNS and SQS in Spring Boot

In an event-driven architecture where multiple microservices need to communicate with each other, the publisher-subscriber pattern provides an asynchronous communication model to achieve this.

Read more

Optimizing Node.js Application Performance with Caching

Endpoints or APIs that perform complex computations and handle large amounts of data face several performance and responsiveness challenges. This occurs because each request initiates a computation or data retrieval process from scratch, which can take time.

Read more