Suppose we have a piece of code that uses an external HTTP service. If we write a test for this code we can invoke the real HTTP service each time we execute the tests. But it might be there is a request limit for the service or the service is not always available when we run the test. With Ratpack it is very, very easy to write a HTTP service that mimics the API of the external HTTP service. The Ratpack server is started locally in the context of the test and we can write extensive tests for our code that uses the HTTP service. We achieve this using the Ratpack EmbeddedApp
or GroovyEmbeddedApp
class. With very little code we configure a server that can be started and respond to HTTP requests.
In our example project we have a class GeocodeService
that uses the external service MapQuest Open Platform Web Services. We use the HTTP Requests library to make a HTTP request and transform the response to an object:
// File: src/main/groovy/mrhaki/geocode/GeocodeService.groovy package mrhaki.geocode import com.budjb.httprequests.HttpClient import com.budjb.httprequests.HttpResponse class GeocodeService { private final HttpClient httpClient private final GeocodeConfig config GeocodeService( final HttpClient httpClient, final GeocodeConfig config) { this.httpClient = httpClient this.config = config } Location getLocation(final Double latitude, final Double longitude) { // Request location details for given latitude and longitude // using a external HTTP service. final HttpResponse response = httpClient.get { uri = "${config.uri}geocoding/v1/reverse".toURI() addQueryParameter 'key', config.apiKey addQueryParameter 'location', [latitude, longitude].join(',') } // Transform JSON result to Map. final Map responseMap = response.getEntity(Map) // Find location specific details in the response. final Map location = responseMap.results[0].locations[0] // Create new Location object. new Location(street: location.street, city: location.adminArea5) } }
The host name and key we need to make a request are set via the GeocodeConfig
class:
// File: src/main/groovy/mrhaki/geocode/GeocodeConfig.groovy package mrhaki.geocode class GeocodeConfig { String apiKey String uri }
And finally a simple POGO to store the location details:
// File: src/main/groovy/mrhaki/geocode/Location.groovy package mrhaki.geocode import groovy.transform.Immutable @Immutable class Location { String street String city }
To access the real MapQuest API service we would set the host and key in the GeocodeConfig
object and we get results from the web service. Now we want to write a Spock specification and instead of accessing the real API, we implement the MapQuest API with Ratpack.
// File: src/test/groovy/mrhaki/geocode/GeocodeServiceSpec.groovy package mrhaki.geocode import com.budjb.httprequests.HttpClient import com.budjb.httprequests.HttpClientFactory import com.budjb.httprequests.jersey2.JerseyHttpClientFactory import ratpack.groovy.test.embed.GroovyEmbeddedApp import ratpack.test.CloseableApplicationUnderTest import spock.lang.AutoCleanup import spock.lang.Specification import spock.lang.Subject import static ratpack.jackson.Jackson.json class GeocodeServiceSpec extends Specification { @AutoCleanup private CloseableApplicationUnderTest mapQuestApi = mapQuestApiServer() @Subject private GeocodeService geocodeService def setup() { final HttpClientFactory httpClientFactory = new JerseyHttpClientFactory() final HttpClient httpClient = httpClientFactory.createHttpClient() // Get address and port for Ratpack // MapQuest API server. final String serverUri = mapQuestApi.address.toString() final GeocodeConfig config = new GeocodeConfig( apiKey: 'secretApiKey', uri: serverUri) geocodeService = new GeocodeService(httpClient, config) } def "get location from given latitude and longitude"() { when: final Location location = geocodeService.getLocation(52.0298141, 5.096626) then: with(location) { street == 'Marconibaan' city == 'Nieuwegein' } } private GroovyEmbeddedApp mapQuestApiServer() { // Create a new Ratpack server, with // a single handler to mimic MapQuest API. GroovyEmbeddedApp.fromHandlers { get('geocoding/v1/reverse') { // Extra check to see if required parameters // are set. This is optional, we could also // ignore them in this stub implementation. if (!request.queryParams.key) { response.status = 500 response.send('Query parameter "key" not set') return } if (!request.queryParams.location) { response.status = 500 response.send('Query parameter "location" not set') return } // Create a response, like the real API would do. // In this case a fixed value, but we could do // anything here, for example different responses, based // on the location request parameter. final Map response = [results: [ [locations: [ [street: 'Marconibaan', adminArea5: 'Nieuwegein']]]]] render(json(response)) } } } }
To run our test we only have to add Ratpack as a dependency to our project. The following example Gradle build file is necessary for this project:
// File: build.gradle apply plugin: 'groovy' repositories { jcenter() } dependencies { compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.4.7' // HttpRequests library to access HTTP services. compile group: 'com.budjb', name: 'http-requests-jersey2', version: '1.0.1' testCompile group: 'org.spockframework', name: 'spock-core', version: '1.0-groovy-2.4' // Include this Ratpack dependency for the GroovyEmbeddedApp class, // we need in the specification. testCompile group: 'io.ratpack', name: 'ratpack-groovy-test', version: '1.3.3' }
Ratpack makes it so easy to create a new HTTP service and in this case use it in a test.
Written with Ratpack 1.3.3.