Search

Dark theme | Light theme

March 2, 2017

Ratpacked: Using Spring Cloud Contract To Implement Server

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.