Search

Dark theme | Light theme

June 15, 2016

Ratpacked: Stub External HTTP Service

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.