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.