At the Big Nerd Ranch, we create Android applications that talk to REST APIs (often written in-house by the Rails backend team). To build these projects with confidence, having tests that verify the interaction with the REST service are critical.
I've blogged already about my recommended software test stack and setup (have a look at this article for details), but I haven't yet talked about how we can have tests which allow us to interact with our webservices while at the same time keeping our tests fast, and our code simple - that's the point of this article!
First, why not test against the live services themselves? After all, doesn't this ensure nothing broke on the server-side we're dependent upon? While this is certainly true - and it may be that we have a test group of integration tests which do hit the live REST API, we don't want this to be part of our main test suite. A few reasons we will want to "mock" (if you're puzzled by what i mean by "mock" check out http://en.wikipedia.org/wiki/Mock_object) these interactions instead:
- We may have a certain number of requests we're limited to for the api we're using. Every time our tests run, it will weigh against that limit.
- Tests that have to hit the remote network to pass or fail will invariably slow our suite down
- The service may be down temporarily. We don't want our test suite to fail because the server was rebooted!
Enter WireMock
An easy to use solution for avoiding live requests to actual REST services is to instead mock the responses from the server. I've found a library called called WireMock that easily facilitates this task. WireMock is designed to allow us to record requests to a webserver, and then programmatically play back those recordings when requests match a particular url pattern. We can define what urls we want WireMock to match in our tests like so:
stubFor(get(urlMatching("/api/.*"))
.willReturn(aResponse()
.withStatus(200)
.withBodyFile("atlanta-conditions.json")));
It lets us completely avoid building one-off test "Adapter"" objects for our service layer just to support the tests - in other words by using WireMock, we can use the live service classes, but get canned responses that don't hit a live webservice, with a minimum of glue code to support this test behavior!
Getting Started
With our new Android Studio project, we'll need to define our library dependencies in our project's gradle file. Here's the configuration i'm using - which relies on robolectric as a test runner setup for android. If you don't know how to go about setting this up in Android Studio, i have a boilerplate project for getting that setup right Here On Github
Gradle Setup
dependencies {
compile 'com.squareup.retrofit:retrofit:1.7.1'
compile 'com.squareup:dagger:0.9.1'
provided 'com.squareup:dagger-compiler:0.9.1'
testCompile 'com.github.tomakehurst:wiremock:1.51:standalone'
testCompile 'org.robolectric:robolectric:2.3'
testCompile 'org.easytesting:fest:1.0.16'
testCompile 'junit:junit:4.+'
testCompile 'com.squareup:fest-android:1.0.8'
}
notice testCompile 'com.github.tomakehurst:wiremock:1.51:standalone'
- i wasn't able to get wiremock to "play nice" (clashing dependencies) with robolectric without the standalone classifier, so make sure you include it in your gradle dependencies list, as you see above.
Our Service Layer
Let's imagine we've got Service Interface and Manager classes that makes a request to Weather Underground to give us the weather data for a particular city. The code example below uses Retrofit to define the webservice endpoints and interface, but if you don't know how retrofit works yet, don't worry too much about it - the focus here is the test setup itself.
WeatherServiceInterface.java:
public interface WeatherServiceInterface {
//defines the http method we want to use using retrofit's handy syntax
//in this case, we want to
//hit api.wunderground.com/api/APIKEY/conditions/q/CA/Atlanta
@GET("/conditions/q/CA/{location}.json")
public ConditionsServiceResponse getConditions(@Path("location") String location);
}
WeatherServiceManager.java:
public class WeatherServiceManager {
private String mWeatherServiceEndpoint;
private final WeatherServiceInterface mWeatherServiceInterface;
public WeatherServiceManager(String weatherServiceEndpoint) {
mWeatherServiceEndpoint = weatherServiceEndpoint;
mWeatherServiceInterface = buildRestAdapter()
.create(WeatherServiceInterface.class);
}
//uses the getConditions(String location) interface, with "Atlanta" for the location path attribute
public List<WeatherCondition> getConditionsForAtlanta() {
return mWeatherServiceInterface.getConditions("Atlanta")
.getConditionsResponse()
.getWeatherConditions();
}
//a retrofit rest adapter - read more about this at:
//http://square.github.io/retrofit/javadoc/retrofit/RestAdapter.Builder.html
private RestAdapter buildRestAdapter() {
return new RestAdapter.Builder()
.setEndpoint(mWeatherServiceEndpoint)
.build();
}
}
ConditionsServiceResponse.java
maps the json response from Weather Underground (using a library called GSON which Retrofit handily supports out of the box) that comes back from the server for that particular request - in this case the weather for Atlanta.
public class ConditionsResponse {
@SerializedName("results")
public List<WeatherCondition> mWeatherConditions;
}
Finally, in use, here's what it looks like:
String serviceEndpoint = "http://api.wunderground.com/api/" + BuildConfig.WEATHERVIEW_API_KEY + "/";
List<WeatherConditions> conditions = new WeatherServiceManager(serviceEndpoint).getConditionsForAtlanta();
Writing the Test
Ok, now that we've explained the service and how it's put together we're ready to start explaining how the mocking for the webservice test is put together. Here's our test setup:
@RunWith(RobolectricTestRunner.class)
class WeatherServiceManagerTest{
public WeatherServiceManager mWeatherServiceManager;
@Rule
public WireMockRule wireMockRule = new WireMockRule(1111);
@Before
public void setup() {
String serviceEndpoint = "http://localhost:1111/api/" + BuildConfig.WEATHERVIEW_API_KEY + "/";
mWeatherServiceManager = new WeatherServiceManager(serviceEndpoint);
}
}
A couple key things about this setup code. The first is our WireMockRule definition.
This will tell the WireMock library to spin up a server instance, which runs locally, on port 1111.
We also pass in a serviceEndpoint resolving to localhost instead of the live api endpoint.
Next, we'll add a test that asserts the number of results we expected would come back, actually came back.
@RunWith(RobolectricTestRunner.class)
class WeatherServiceManagerTest{
...
@Test
public void testGetCurrentWeatherReturnsExpected() {
stubFor(get(urlMatching("/api/.*"))
.atPriority(5)
.willReturn(aResponse()
.withStatus(200)
.withBodyFile("atlanta-conditions.json")));
List<WeatherCondition> conditionsForAtlanta = mWeatherServiceManager.getConditionsForAtlanta();
assertThat(conditionsForAtlanta.size()).isEqualTo(1);
}
...
}
What this setup says is that any time i make a request matching "/api/*", return a response with 200 as its status, containing the content within the file "atlanta-conditions.json" as it's response body!
By default, wiremock will pull that file from : test/resources/__files/ - so add a new file called atlanta-conditions.json. I copied a response from the live webservice API earlier into that file:
{
"response": {
"version":"0.1",
"termsofService":"http://www.wunderground.com/weather/api/d/terms.html",
"features": {
"conditions": 1
}
, "results": [
{
"name": "Atlanta",
"city": "Atlanta",
"state": "GA",
"country": "US",
"country_iso3166":"US",
"country_name":"USA",
"zmw": "30301.1.99999",
"l": "/q/zmw:30301.1.99999"
}
]
}
}
With the addition of this file, we can then run our test - which should pass. So - what have we done? We've mocked the response from the webserver, decoupling our test from dependence on a live webserver, and provided a canned response that speeds up our test suite because it's local data, doesn't incur a hit against the api limit, and is immune to the service going down.
Example Code: Weatherview
I hope this article helped to outline how mocking a REST webservice can be accomplished easily by using WireMock! I wrote a quick example project of how you can use WireMock to test your services that i've put Right Here on github. Check it out (and let me know if you have questions or ideas about mocking with webservices)!