Spring Cloud Contract is a project that allows to write a contract for a service using a Groovy DSL. In the contract we describe the expected requests and responses for the service. From this contract a stub is generated that can be used by a client application to test the code that invokes the service. Spring Cloud Contract also generates tests based on the contract for the service implementation. Let's see how we can use the generated tests for the service implementation for a Ratpack application.
Spring Cloud Contract comes with a Gradle plugin. This plugin adds the task generateContractTests
that creates tests based on the contract we write. There are also tasks to create the stub for a client application, but here we focus on the server implementation. In the following Gradle build file for our Ratpack application we use the Spring Cloud Contract Gradle plugin. We configure the plugin to use Spock as framework for the generated tests.
buildscript { ext { verifierVersion = '1.0.3.RELEASE' } repositories { jcenter() } dependencies { // We add the Spring Cloud Contract plugin to our build. classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifierVersion}" } } plugins { id 'io.ratpack.ratpack-java' version '1.4.5' id 'com.github.johnrengelman.shadow' version '1.2.4' // The Spring Cloud Contract plugin relies on // the Spring dependency management plugin to // resolve the dependency versions. id 'io.spring.dependency-management' version '1.0.0.RELEASE' } apply plugin: 'spring-cloud-contract' repositories { jcenter() } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${verifierVersion}" } } dependencies { runtime 'org.slf4j:slf4j-simple:1.7.24' testCompile 'org.codehaus.groovy:groovy-all:2.4.9' testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' testCompile 'org.spockframework:spock-spring:1.0-groovy-2.4' testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier' testCompile 'commons-logging:commons-logging:1.2' } mainClassName = 'mrhaki.sample.PirateApp' assemble.dependsOn shadowJar /************************************************************** * Configure Spring Cloud Contract plugin *************************************************************/ contracts { // Of course we use Spock for the generated specifications. // Default is JUnit. targetFramework = 'Spock' // With explicit testMode real HTTP requests are sent // to the application from the specs. // Default is MockMvc for Spring applications. testMode = 'Explicit' // Base class with setup for starting the Ratpack // application for the generated specs. baseClassForTests = 'mrhaki.sample.BaseSpec' // Package name for generated specifications. basePackageForTests = 'mrhaki.sample' }
It is time to write some contracts. We have a very basic example, because we want to focus on how to use Spring Cloud Contract with Ratpack and we don't want to look into all the nice features of Spring Cloud Contract itself. In the directory src/test/resources/contracts/pirate
we add a contract for the endpoint /drink
:
// File: src/test/resources/contracts/pirata/drink.groovy package contracts.pirate import org.springframework.cloud.contract.spec.Contract Contract.make { request { method 'GET' urlPath '/drink', { queryParameters { parameter 'name': $(consumer(regex('[a-zA-z]+')), producer('mrhaki')) } } headers { contentType(applicationJson()) } } response { status 200 body([response: "Hi-ho, ${value(consumer('mrhaki'), producer(regex('[a-zA-z]+')))}, ye like to drink some spiced rum!"]) headers { contentType(applicationJson()) } } }
We add a second contract for an endpoint /walk
:
// File: src/test/resources/contracts/pirata/walk_the_plank.groovy package contracts.pirate import org.springframework.cloud.contract.spec.Contract Contract.make { request { method 'POST' urlPath '/walk' body([name: $(consumer(regex('[a-zA-z]+')), producer('mrhaki'))]) headers { contentType(applicationJson()) } } response { status 200 body([response: "Ay, matey, ${value(consumer('mrhaki'), producer(regex('[a-zA-z]+')))}, walk the plank!"]) headers { contentType(applicationJson()) } } }
The last step for generating the Spock specifications based on these contracts is to define a base class for the tests. Inside the base class we use Ratpack's support for functional testing. We define our application with MainClassApplicationUnderTest
and use the getAddress
method to start the application and to get the port that is used for the application. The generated specifications rely on RestAssured to invoke the HTTP endpoints, so we assign the port to RestAssured
:
// File: src/test/groovy/mrhaki/sample/BaseSpec.groovy package mrhaki.sample import com.jayway.restassured.RestAssured import ratpack.test.MainClassApplicationUnderTest import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification abstract class BaseSpec extends Specification { @Shared @AutoCleanup def app = new MainClassApplicationUnderTest(PirateApp) def setupSpec() { final URI address = app.address RestAssured.port = address.port } }
We can write the implementation for the PirateApp
application and use Gradle's check
tasks to let Spring Cloud Contract generate the specification and run the specifications. The specification that is generated can be found in build/generated-test-sources
and looks like this:
// File: build/generated-test-sources/contracts/mrhaki/sample/PirateSpec.groovy package mrhaki.sample import com.jayway.jsonpath.DocumentContext import com.jayway.jsonpath.JsonPath import static com.jayway.restassured.RestAssured.given import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson class PirateSpec extends BaseSpec { def validate_drink() throws Exception { given: def request = given() .header("Content-Type", "application/json") when: def response = given().spec(request) .queryParam("name", "mrhaki") .get("/drink") then: response.statusCode == 200 response.header('Content-Type') ==~ java.util.regex.Pattern.compile('application/json.*') and: DocumentContext parsedJson = JsonPath.parse(response.body.asString()) assertThatJson(parsedJson).field("response").matches( "Hi-ho, [a-zA-z]+, ye like to drink some spiced rum!") } def validate_walk_the_plank() throws Exception { given: def request = given() .header("Content-Type", "application/json") .body('''{"name":"mrhaki"}''') when: def response = given().spec(request) .post("/walk") then: response.statusCode == 200 response.header('Content-Type') ==~ java.util.regex.Pattern.compile('application/json.*') and: DocumentContext parsedJson = JsonPath.parse(response.body.asString()) assertThatJson(parsedJson).field("response").matches("Ay, matey, [a-zA-z]+, walk the plank!") } }
If we run Gradle's check
task we can see the Spring Cloud Contract plugin tasks are executed as well:
$ gradle check :copyContracts :generateContractTests :compileJava :compileGroovy NO-SOURCE :processResources NO-SOURCE :classes :compileTestJava NO-SOURCE :compileTestGroovy :processTestResources :testClasses :test :check BUILD SUCCESSFUL Total time: 5.749 secs
The code for the complete application is on Github.
Written with Ratpack 1.4.5 and Spring Cloud Contract 1.0.3.RELEASE.