Chriskoeberle androidendpointintegrationtestswithmockedendpoints cover

Android Endpoint Integration Tests with Mocked Endpoints

If you enjoy talks from 360 AnDev, please support the conference via Patreon!

Android developers often fetch data from HTTP endpoints to display it on the device. These endpoints can be a major source of frustration, especially when they are backed by complex legacy systems. One easy source of testing that will actually speed the pace of development from the beginning is end-to-end integration tests with those endpoints. However, in the end the tests can only be as stable as the environment they’re testing. I shall present a framework for creating these tests and partially automate creating an OkHttp interceptor that will provide mocked responses to ensure the stability of the tests. With this framework and this interceptor, integration tests can distinguish between a failure caused by a change to the behavior of the endpoint and a failure caused by a change to the application source.


Introduction

I’m Chris Koeberle. I’m an Android developer at Bottle Rocket. We’re an agency. We make apps for clients. What we’re going to talk about today is the problem that I faced, the requirements for implementing my solution, how I implemented the solution, and then mocking the responses for the network calls that I test. This is going to be code-heavy, but you can find the code at this link.

In my Android development career, I have mainly worked on travel sector apps, which means that I’m working against the backend that has a backend that has a backend that’s controlled by someone that nobody knows who it is.

The problem

There are rules that nobody has documented, but when you try and make a purchase, all of a sudden, you get back 500 Internal Server Error and that’s the entirety of your explanation for where you went wrong. My best example of that was the server told me that my user had a gender of MALE (capital M, capital A, capital L, capital E).

I turned around and I tried to make a purchase with that user with a gender of all caps male, 500 Internal Server Error. It turns out, and at least someone was available to explain this to us once we figured out, found out that we had a problem. The server that told us, all caps MALE, actually wanted capital M, lowercase a, lowercase l, lowercase e.

My main goal in initial development is to reduce the iteration time. Instead of having to click all the way through all of my screens to get to where I have a purchase button, I can do it in as little time as possible.

The solution

The worst plan is running the app.

The first time I did a travel sector app, I built up the screen that gives me a list of hotels. I built up the screen that gives me the results and the screen that lets me input the user’s information and the payment information.

I’m finally to the screen and I’m going to make the purchase. And each time I go to a new screen, then I have to make the API calls, test them, get them working. I get to the purchase screen, I click Purchase, and get 500 Internal Server Error. Every time I make a change, I have to build, deploy, and go through everything again.

Then Postman came out, and Postman’s great. I certainly advocate the use of Postman, but one drawback of Postman is that it will export some Java for me, but I don’t have working code in my app that will do the same thing. Charles and Fiddler are helpful too, but again, my goal is to end up with working code at the exact same moment that I get the call to the backend working.

First, I write a bunch of integration tests. In principle (and this is what I did for the app that I’m currently working on), you can write your entire networking layer in an Android app that has zero activities.

I spent two weeks building 40 endpoints, getting the purchase flow working, at least for enough cases that we could start building a UI against it, and then we started working on UI. Since I’m building these integrations tests, I’d also like them to have some ongoing value, but the problem with that is our servers are flaky and the server that worked last week isn’t going to work this week. We have on our whiteboard, “Use QA2 today.” And sometimes we erase the QA2 and we don’t know what to put. We need to be able to see that everything we wrote is still working even though there’s a remote failure.

Also, sometimes you have an endpoint that has side effects. You don’t want to book every room at a hotel, even on QA. If you’re enrolling a new user every time someone pushes, someone is probably going to complain at some point.

Requirements

Our goal is to mock the results that we’re getting back from the server so that we can rely on those even when we’re not hitting the server. Because we’re saying the word testing, we need Dependency Injection or Service Locator. We’re going to need OkHttp because I’m using an interceptor. This only works with OkHttp.

We’ll need something that gets us off the main thread. My examples are going to use RxJava. At Bottle Rocket, for a while, we were using something that wasn’t RxJava and it had some Looper problems, so then we had to mock the Looper where the Looper wasn’t releasing threads when it was supposed to. RxJava doesn’t have this problem.

The example

You can follow along here.

I can’t walk through any of the systems that I’ve worked on with their amazing 500 Internal Server errors, but that’s okay because they are more complex examples than we need. I do want to be able to show making a POST call, making a call that changes something on the server.

That would normally require that have login but then I’m going to have to also have the complexity of wrapping login around everything, so we’re going to look at GitHub because GitHub is so friendly, they let us create an anonymous Gist. This example is going to cover most of the difficulties I’ve come up with in the hundreds of endpoints that I have written against.

Dependency injection

Most people use dependency injection, but for this ServiceLocator is going to be easier to read.

Get more development news like this

public class ServiceLocator {  
    Map<Class<?>, Object> mLocatorMap; 
    
    private ServiceLocator() { 
        mLocatorMap=newHashMap<>();
    }
    
    private static class SingletonHolder { 
        static final ServiceLocator instance = new ServiceLocator();
    }
    
    private static ServiceLocator getInstance() {
        returnSingletonHolder.instance; 
    }
 
    public static <T> void put(Class<T> type, T instance) { 
        if (type == null) { throw new NullPointerException(); }
        getInstance().mLocatorMap.put(type, instance); 
    }
    
    public static <T> T get(Class<T> type) { 
        return (T) getInstance().mLocatorMap.get(type); 
    }
}
public class ServiceInjector { 
    public static <T> T resolve(Class<? extends T> type) {
        returnServiceLocator.get(type);  
    }
}

ServiceLocator.put(RxEndpoints.class, new RxEndpointsImpl());
Flowable<User> flowable = ServiceInjector
            .resolve(RxEndpoints.class)
            .getUser("bottlerocketapps");

ServiceInjector.resolve is the call that says we’re getting something out of the ServiceInjector. We’re getting an instance of our RxEndpoints class. And then we make calls against that class.

We’re calling getUser and this will return the GitHub user that is the company I work for.

OkHttp

We also need OkHttp. That needs to be injectable because we’re going to inject a different interceptor depending on what kind of mocking behavior we want.

public class OkHttpClientUtil { 
    private static final long READ_TIMEOUT = 120; 

    public static OkHttpClient getOkHttpClient(Context context, 
        MockBehavior mock) {  OkHttpClient okHttpClient = null; 
        OkHttpClient.Builder builder = new OkHttpClient.Builder(); 
        if(mock != MockBehavior.DO_NOT_MOCK) {
            builder.addInterceptor(new MockedApiInterceptor(context, mock)); 
        } 
        okHttpClient = builder 
            .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) 
            .retryOnConnectionFailure(false) 
            .build(); 
        return okHttpClient; 
    }  
}

...

ServiceLocator.put(OkHttpClient.class, OkHttpClientUtil.getOkHttpClient(null, MockBehavior.MOCK));

...

subscriber.onNext(ServiceInjector.resolve(OkHttpClient.class).newCall(request).execute());

This is where the interceptor goes. It does the mocking. And then we’re putting it in the ServiceLocator. When we make our OkHttp calls, we resolve the client and we make a call against it. This is also a good practice injecting your OkHttp because there are other things that you want to have always on the OkHttp, things that you want to be able to change. I recommend it.

RxJava

I am not an RxJava expert - please do not use this as a tutorial for how to use RxJava. I upgraded the slides to conform to RxJava 2.1.1.

private Flowable<Response> getResponse(final HttpUrl url) {
    return Flowable.fromCallable(new Callable<Response>() {
        @Override
        public Response call() throws Exception {
            System.out.println(url);
            Request request = ServiceInjector.resolve(ServiceConfiguration.class).getRequestBuilder()
                .url(url)
                .build();
            return ServiceInjector.resolve(OkHttpClient.class).newCall(request).execute();
        }
    });
}

@Override 
public Flowable<User> getUser(String userName) { 
    HttpUrl url = ServiceInjector.resolve(ServiceConfiguration.class).getUrlBuilder()
        .addPathSegment(USER) 
        .addPathSegment(userName) 
        .build();  
    return getResponse(url) 
        .flatMap(new FetchString()) 
        .flatMap(new ToJson<User>(UserImpl.class)); 
}

This is showing the Rx code. I have a getResponse method that’s going to get my service configuration, so I use a service configuration so I can switch environments. Build my URL, and then call out OkHttp. getUser is going to use this getResponse. It’s also going to use Fetch String and ToJson:

private class FetchString implements Function<Response, Flowable<String>> {
    @Override
    public Flowable<String> apply(final Response response) {
        return Flowable.fromCallable(new Callable<String>() {
            @Override
            public String call() throws Exception {
                if (!response.isSuccessful()) {
                    throw new IOException(response.message());
                }
                String responseString = response.body().string();
                System.out.println(responseString);
                return responseString;
            } 
        });
    } 
}

private class ToJson<T>  implements Function<String, Flowable<T>> {
    private final Class mTargetClass;
    
    private ToJson(Class mTargetClass) {
        this.mTargetClass = mTargetClass;
    }
    
    @Override
    public Flowable<T> apply(final String s) {
        return Flowable.fromCallable(new Callable<T>() {
            @Override
            public T call() throws Exception {
                return (T) ServiceInjector.resolve(Gson.class).fromJson(s, mTargetClass);
            }
        }); 
    }
}

FetchString does the work of pulling the string out of the response body. It’s totally unchecked in this version, so all of your API calls should succeed and everything will be fine. That’s not good advice. And then we have the ToJson, which again, blindly assumes that we’re going to get exactly what we wanted in our body.

Testing the Observable

The only testing framework that I’m using is JUnit, and this is why my iteration times are three to five seconds. I don’t have to spool up anything. I don’t have to connect to anything. I don’t have to start a server. All I’m using is JUnit.

I change capital MALE to first letter capital Male, and three to five seconds later, or if I’m changing a bunch of calls together, then however long it takes for all the server calls to complete. I have a red light or a green light. We have a few dependencies that are going to stay the same for all the tests.

Our base test is going to set those up, and it’s Before:

public class BaseApiTest {  @Before 
    public void setup() { 
    ServiceLocator.put(RxEndpoints.class, new RxEndpointsImpl());
        ServiceLocator.put(ServiceConfiguration.class, new
        ServiceConfigurationImpl());  
        ServiceLocator.put(Gson.class, GsonUtil.getGson()); 
    }  
}

We’re going to have our endpoints. We’re going to have our ServiceConfiguration, We’re going to have Gson. ServiceConfiguration can let us change environments. Obviously we don’t have a staging environment for GitHub, at least I don’t. Gson, again, is not a recommendation, it’s just something that makes setting up examples fast.

Each test is going to inject its own HTTP client. This way, each test can control how it’s mocked. And again, we’re not writing unit tests. Our goal isn’t to generate exhaustive coverage. Our goal is to make sure that the call completed, to make sure that the call returns something that is like what we expected.

@Test
public void testOrganization() {
    ServiceLocator.put(OkHttpClient.class, OkHttpClientUtil.getOkHttpClient(null, MockBehavior.MOCK));
    Flowable<Organization> flowable = ServiceInjector.resolve(RxEndpoints.class).getOrg("bottlerocketstudios");
    TestSubscriber<Organization> testSubscriber = new TestSubscriber<>();
    flowable.subscribe(testSubscriber);
    testSubscriber.assertComplete();
    List<Organization> orgList = testSubscriber.values();
    assertEquals(orgList.size(), 1);
    assertEquals(orgList.get(0).getName(), "Bottle Rocket Studios");
}

We’re getting the Bottle Rocket Studios organization. We are asserting that the call completed. We’re asserting that we got one result. We’re asserting that the name of the result we got is Bottle Rocket Studios. This is going to make your code coverage unreliable when you run these tests. I had something like 8% code coverage from these tests because it’s hitting all of the code that I use to set up all of my endpoints. It’s hitting a lot of the logic that runs through the app, but it’s not testing it. It’s only testing, “Can I successfully get some result back from the server?” If you’re a slave to code coverage, you probably want to disable these for your coverage reports.

Then we’re going to test the Observable when we chain a call. This is the beginning of a list detail flow:

public class GistTest extends BaseApiTest {
    @Test
    public void testAllGists() {
        ServiceLocator.put(OkHttpClient.class, OkHttpClientUtil.getOkHttpClient(null, MockBehavior.MOCK));
        Flowable<Gist[]> flowable = ServiceInjector.resolve(RxEndpoints.class).getGists();
        TestSubscriber<Gist[]> testSubscriber = new TestSubscriber<>();
        flowable.subscribe(testSubscriber);
        testSubscriber.assertComplete();
        List<Gist[]> gists = testSubscriber.values();
        Gist gist = gists.get(0)[0];
        Flowable<Gist> gistFlowable = ServiceInjector.resolve(RxEndpoints.class).getGist(gist.getId());
        TestSubscriber<Gist> gistTestSubscriber = new TestSubscriber<>();
        gistFlowable.subscribe(gistTestSubscriber);
        Gist detailGist = (Gist) gistTestSubscriber.values().get(0);
        assertEquals(detailGist.getDescription(), gist.getDescription());
    } 
}

We’re going to call getGists and we’re going to get the Gists on GitHub in order from the first one. I think it naturally cuts off at 20. We’ll get a list for 20 Gists and then we’re going to pick the first one from that and we’re going to do the detail call on it.

We pull out the ID, we do getGist. That’s going to give us a more detailed view of that Gist. And then we’re going to check. And this is the only assert that matters is the last line there, assertEquals. We want the detail description to match the description that we got from the list result. We’re checking to make sure that this detail call succeeded, making sure that it matches what we expected it to match.

A Mocked Interceptor

Next up is my mocking interceptor, and that’s in the repository but I also put a link to the Gist that has that file.

public abstract class AbstractMockedApiInterceptor implements Interceptor {  
    public AbstractMockedApiInterceptor(Context context, MockBehavior mock) { 
        if (context != null) { mContext = context.getApplicationContext(); } else { mContext = null; } 
        mMockBehavior = mock; 
    }
 
    @Override 
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();  
        NetworkCallSpec mockFound = null; 
        for (NetworkCallSpec spec : mResponseList) { 
            if (spec.matches(request.url(), request.method(), stringifyRequestBody(request))) {  
                mockFound = spec; 
                Response.Builder builder = new Response.Builder(); 
                String bodyString = resolveAsset("mocks/"+spec.mResponseFilename); 
                bodyString = substituteStrings(bodyString, request); 
                if(bodyString != null) { 
                    ResponseBody body = ResponseBody.create(MediaType.parse(getMockedMediaType()), bodyString); 
                    builder = builder.body(body).request(request).protocol(Protocol.HTTP_1_1).code(spec.mResponseCode).message(spec.mResponseMessage); 
                } 
                if (!ignoreExistingMocks()) { 
                    noteThatThisFileWasUsed(spec.mResponseFilename); 
                    return builder.build(); 
                } 
            } 
        } 
        if (fetchNetworkResults()) { 
            Response response = chain.proceed(request); 
            response = memorializeRequest(request, response, mockFound); 
            return response; 
        } 
        throw new IOException("Unable to handle request with current mocking strategy"); 
    }
    
    protected boolean fetchNetworkResults() { return mMockBehavior != MockBehavior.MOCK_ONLY; }  
    protected boolean ignoreExistingMocks() { return mMockBehavior == MockBehavior.LOG_ONLY; }  
    protected String getMockedMediaType() { return "json"; }
}

Right there at the top, we’re looking for a context and if we don’t have a context, we’re not saving it. The reason we need a context is because I want these mocks to be able to run on the device.

Thing that this ends up being useful for is when we get changes on the server on our flaky servers, the first thing we do is we check to make sure that they’re up, and if they’re up, then we write our tests against them. We grab the mocks, we grab sufficient mocks that we can use the app and get to that state.

I’m no longer sensitive to the concerns of the other developers on the project when they say, “I can’t work on this because the server is down” or, “The server that has the changes that we need to test against is down.” After months of telling me that, they have finally been won over to this idea. The first thing they do is they grab mocks and now, they can get to the screens that they need to change, they can test the changes without having to rely on a server. And now that they’ve finally been won over, they’re happy that they do this.

We’re also going to set off a MockBehavior. The mock behaviors that I’m supporting are MOCK_ONLY, LOG_ONLY, MOCK, and DO_NOT_MOCK. If we’re doing DO_NOT_MOCK, then we don’t even add the interceptor, so I’m not testing against that here. If we’re doing MOCK, then we try to make the request. We try to find a mocked version of the request. If we can’t find it, if nothing that we’ve mocked matches, then we’re going to return the actual network call and we’re going to save that network call to make it easier for you to add the mock.

That’s what the fetchNetworkResults helper method there is is it’s asking, “Should we go to the network?” And then sometimes we only want to log the calls that we get from the network because we know that our call isn’t going to succeed if we use any mocked calls. I do that a lot when I’m setting up mocks for a complete flow, and I’ll explain more about that in a bit.

public abstract class AbstractMockedApiInterceptor implements Interceptor {  
    @Override 
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request(); 
        NetworkCallSpec mockFound = null; 
        for (NetworkCallSpec spec : mResponseList) { 
            if (spec.matches(request.url(), request.method(), stringifyRequestBody(request))) {  
                mockFound = spec; 
                Response.Builder builder = new Response.Builder(); 
                String bodyString = resolveAsset("mocks/"+spec.mResponseFilename); 
                bodyString = substituteStrings(bodyString, request); 
                if(bodyString != null) { 
                    ResponseBody body = ResponseBody.create(MediaType.parse(getMockedMediaType()), bodyString); 
                    builder = builder.body(body).request(request).protocol(Protocol.HTTP_1_1).code(spec.mResponseCode).message(spec.mResponseMessage); 
                } 
                if (!ignoreExistingMocks()) {
                    noteThatThisFileWasUsed(spec.mFilename);  
                    return builder.build(); 
                } 
            } 
        } 
        if (fetchNetworkResults()) { 
            Response response = chain.proceed(request); 
            response = memorializeRequest(request, response, mockFound);  
            return response; 
        } 
        throw new IOException("Unable to handle request with current mocking strategy"); 
    }  
}

The basic flow of the interceptor is it’s going to loop through a whole list of response specifications. If one matches, it’s going to build a response from it and return it. And if none of them match, then we’ll make the network call. I’m going to pause for a second and mention OkReplay. OkReplay came out in between me submitting this talk and gave me a heart attack, but OkReplay depends on Espresso and it’s not solving exactly the same problem. It doesn’t require as much customization – you get your tapes out and feed your tapes in. It is certainly easier to use for the problem it solves.

I don’t have a package depot and a nice repository that you can drop in. I loop through all of my network specs, er, my network call specs, and I see if they match. If they match, then I’m going to build up a response and pass it back. If they don’t match, then I fall through and hit the network. And then I make that memorializeRequest call that’s going to build up what I need to mock it in the future.

Matching Requests

The way I look to see if something matches, the first five fields on NetworkCallSpec are the RequestURL (and that’s a pattern, I can do pattern matching on it), the RequestMethod, the QueryParameters, the RequestBody, and RequestBodyContains, since most of the time, matching the entire RequestBody is not the right plan. Most of the time, you want to look for something in the RequestBody.

public static class NetworkCallSpec { 
    private final String mRequestUrlPattern; 
    private String mRequestMethod; 
    private Map<String, String> mRequestQueryParameters;  
    private String mRequestBody; 
    private Set<String> mRequestBodyContains; 
    
    private int mResponseCode; 
    private String mResponseMessage;  
    private final String mResponseFilename; 
    
    public boolean matches(HttpUrl url, String method, String body) {
        if (!url.encodedPath().matches(mRequestUrlPattern)) { return false; } 
        if (!mRequestMethod.equalsIgnoreCase(method)) { return false; } 
        if (mRequestMethod.equalsIgnoreCase("POST") && !TextUtils.isEmpty(mRequestBody) && !mRequestBody.equalsIgnoreCase(body)) { 
            return false;
        }
        
...

Every time this user makes a purchase against this property, we’re going to give this result. We don’t care what dates they’re requesting. We want to, this test that uses this user to succeed and give a different result than this test that uses this user at a different property. We look to see if the path matches. We look to see if we’re doing the same method because we don’t want to return 204 Created when we’re doing a GET.

If we’re doing a POST, then we want to check and make sure that if we specified a whole body that that exact body is what we’re sending. I rarely use that because that’s not resilient to changes in the order of things in JSON. If you add a new field, then all of a sudden, this stops working. So I use RequestBodyContains:

...

    for (String contains : mRequestBodyContains) {  
        if (!body.contains(contains)) { 
            return false;  
        } 
    } 
    for (Map.Entry<String, String> kvp : mRequestQueryParameters.entrySet()) { 
        boolean foundKey = false; 
        boolean foundValue = false; 
        for (String key : url.queryParameterNames()) { 
            if (key.matches(kvp.getKey())) {  
                foundKey = true; 
                String value = url.queryParameter(key); 
                if (value != null && value.matches(kvp.getValue())) { 
                    foundValue = true;
                }
                if (value == null && (kvp.getValue() == null || kvp.getValue() == "" || kvp.getValue().equalsIgnoreCase("null"))) {
                    foundValue = true; 
                }
            }
        }
        if (!foundKey || !foundValue) {
            return false;
        }
    }
    return true;
}

I loop to see if the body contains this exact string. I have chunks of JSON that find the username, that find the dates, whatever I’m looking for. And then with the query parameters, these are a way of excluding things. If you don’t put any query parameters, then any set of query parameters will match. If you only want to match when the user is logging in with ID Chris, then you would put ID equals Chris, and it will match for any ID equals Chris, but it will ignore what you’re passing in as the password. Also, don’t pass passwords as get parameters.

If any of these fail, it’s going to return false. If everything matches or doesn’t have enough information to conflict, then it will return true, and then we will build up a response and send it back.

Building Responses

When we’re running as a JUnit test, we don’t have a context, so we’re going to go into test/resources/mocks/. If we’re running the app, then we’re going to read it from assets/mocks/. I use a build.gradle Copy task and I copy from assets/ into test/resource/. And Git ignore that so that it doesn’t put two copies.

When I am making a mock, I copy something into assets/mocks/. It’s grabbing the body out of the file. It’s doing a substituteStrings call that we’ll talk about in a second. It’s building up the response code, the response message, and sending it back to whoever called it.

Response.Builder builder = new Response.Builder(); 
String bodyString = resolveAsset("mocks/"+spec.mFilename);  
bodyString = substituteStrings(bodyString, request);  
if(bodyString != null) { 
    ResponseBody body = ResponseBody.create(MediaType.parse(getMockedMediaType()), bodyString); 
    builder = builder.body(body).request(request).protocol(Protocol.HTTP_1_1).code(spec.mResponseCode).message(spec.mResponseMessage); 
} 
if (!ignoreExistingMocks()) { 
    noteThatThisFileWasUsed(spec.mFilename); 
    return builder.build();  
}

private String resolveAsset(String filename) {  
    if (mContext != null) { 
        return getAssetAsString(mContext, filename);  
    } else { 
        try { 
            return readFromAsset(filename); 
        } catch (IOException e) { 
            Timber.e(e, "Error reading from asset - this should only be called in tests.");  
        }
    }
    return null; 
}

Saving New Mocks

When we want to create a new mock, what ends up happening is we run the test that we’re working on. It retrieves a file and then it saves it out so we can mock it. It’s going to save it out in two places. It’s going to save the file itself, the body response, in a file. If we’re running on the device, then it will save it in a directory within the user dir. If we’re running it locally in a test, then it’s going to save it in the root directory. But then we’re also going to build up the code, to register with our interceptor what the requirement is to return this mock.

private Response memorializeRequest(Request request, Response response, NetworkCallSpec mockFound) {  
Response.Builder newResponseBuilder = response.newBuilder(); 
try { 
    String responseString = response.body().string(); 
    List<String> segments = request.url().encodedPathSegments(); 
    String endpointName = segments.get(segments.size() - 1); 
    String callSpecString = "mResponseList.add(new NetworkCallSpec(\""+request.url().encodedPath()+"\", \"::REPLACE_ME::\")";  
    if (response.code() != HttpURLConnection.HTTP_OK) { 
        callSpecString += ".setResponseCode("+response.code()+")"; 
        endpointName += "-"+response.code(); 
    } 
    if (!TextUtils.isEmpty(response.message()) && !response.message().equalsIgnoreCase("OK")) { 
        callSpecString += ".setResponseMessage(\""+response.message()+"\")"; 
    } 
    if (!request.method().equalsIgnoreCase("GET")) { 
        callSpecString += ".setRequestMethod(\""+request.method()+"\")";  
        endpointName += "-"+request.method(); 
    } 
    if (request.url().querySize()>0) { 
        for (String key : request.url().queryParameterNames()) { 
            callSpecString += ".addRequestQueryParameter(\""+key.replace("[", "\\\\[").replace("]", "\\\\]")+"\", \""+request.url().queryParameter(key)+"\")";  
        } 
    }
    String body = stringifyRequestBody(request);  
        if (body != null) { 
            callSpecString += ".addRequestBody(\""+body.replace("\"", "\\\"").replace("\\u003d", "\\\\u003d")+"\")";  
            endpointName += "-"+body.hashCode(); 
        } 
        requestSpecString += ");"; 
        if (endpointName.length()>100) { 
            endpointName = ""+endpointName.hashCode(); 
        } 
        endpointName = getUniqueName(endpointName); 
        callSpecString = callSpecString.replace("::REPLACE_ME::", endpointName); 
        if (mockFound != null) { 
            callSpecString += " // duplicate of existing mock "+mockFound.mPattern;  
            if (!TextUtils.isEmpty(mockFound.mRequestBody)) { 
                callSpecString += " with body "+mockFound.mRequestBody; 
            } 
        } 
        callSpecString += "\n"; 
        writeToFile(callSpecString, responseString, endpointName);
        newResponseBuilder.body(ResponseBody.create(response.body().contentType(), responseString)); 
    } catch (IOException e) { 
        Timber.e("Unable to save request to "+request.url().toString()+" : ", e);  
    }
    return newResponseBuilder.build(); 
}

It adds everything that you could potentially want. It will add the full path, it will add all the query parameters. It will add the exact body, it will add the method, assuming that it’s not GET. And then you can delete the things that you don’t need. You can go in and you can change the body to a bodyContains. If there are parameters that you don’t care about, you can get rid of those.

It will save the entire body to the POST request. It also makes the filename unique. We have one endpoint that returns, the end of the path is the user’s ID. It’s not returning the user, it’s returning something based on query parameters, but every time I make that call, I get a new mock that is the user’s ID, dash one, dash two, dash three.

If it’s long, then I put out the hash code because sometimes we have an endpoint that has a length of over a hundred characters and that’s obnoxious.

Substitutions

One other thing that I run into a lot in booking flows is I get something that is valid for tomorrow and then it stops being tomorrow and my test stops working. Even worse, if you’re doing a login, you’re going to get back an authentication token and you’re getting back an expiration date. Six weeks later, all of a sudden your test isn’t working because you try to login and it says, “you’re logged in until yesterday. Enjoy.” What we can also put in the call spec is a substitution pattern.

private static interface StringSubstitutor { 
    String replaceOneString(String body, Request request);  
    boolean matchesFound(String body); 
}
private String substituteStrings(String bodyString, Request request) { 
    // Because each match can get replaced with something different, we have to reset the matcher after every replacement.  
    // This way of doing things happens to enforce this in a non-obvious way, because we create a new matcher every time.  
    for (StringSubstitutor substitutor : mSubstitutorList) { 
        while (substitutor.matchesFound(bodyString)) { 
            bodyString = substitutor.replaceOneString(bodyString, request);  
        }
    }
    return bodyString; 
}       
private static final Pattern DATE = Pattern.compile(%DATE[^%]*%");
private static class DateSubstitutor implements StringSubstitutor { 
    @Override 
    public String replaceOneString(String body, Request request) { 
        Matcher dateMatcher = DATE.matcher(body);  
        dateMatcher.find(); 
        String match = dateMatcher.group(); 
        Map<String, String> query = getQueryFromUri(match);  
        LocalDate date = new LocalDate(); 
        if(query.containsKey(OFFSET_PARAMETER)) { 
            date = date.plusDays(Integer.parseInt(query.get(OFFSET_PARAMETER))); 
        } 
        body = dateMatcher.replaceFirst(date.toString());  
        return body; 
    } 
}

I have a substitution pattern for date, a substitution pattern for date time, and then I have one that goes off the parameters. I can pull a query parameter, “You were looking for a hotel reservation that started on August 1st”. I’m going to replace every instance of the reservation date in the body with August 1st so that it makes sense.

The format is %DATE. And then down at the bottom, you can see that, for instance, if I’m adding an offset, then I do question mark, offset equals 30. That moves the date 30 days into the future. If I’m doing query parameters, then the key is going to be the parameter. This lets me make my responses more resilient to changes in time. We have one response that has to be 30 minutes into the future. We care about what happens when that response is expired. We make it one minute into the past.

Back to the Test with Side Effects

Here is our test that has side effects:

private static final String CREATE_FILE_NAME = "AbstractMockedInterceptor.java"; 
private static final String CREATE_DESCRIPTION = "An OkHttp Interceptor that returns mocked results if it has them.";  
@Test 
public void createGist() throws IOException { 
    ServiceLocator.put(OkHttpClient.class,
    OkHttpClientUtil.getOkHttpClient(null, MockBehavior.MOCK_ONLY)); 
    Gist gist = new GistImpl(); 
    gist.setDescription(CREATE_DESCRIPTION); 
    gist.addFile(CREATE_FILE_NAME, readFromAsset("mocks/javaclass")); 
    Observable<Gist> observable = ServiceInjector.resolve(RxEndpoints.class).createGist(gist); 
    TestSubscriber<Gist> testSubscriber = new TestSubscriber<>(); 
    observable.subscribe(testSubscriber); 
    testSubscriber.assertCompleted(); 
    List<Gist> gistList = testSubscriber.getOnNextEvents(); 
    Gist resultGist = gistList.get(0); 
    Observable<Gist> gistObservable = ServiceInjector.resolve(RxEndpoints.class).getGist(resultGist.getId());  
    TestSubscriber<Gist> gistTestSubscriber = new TestSubscriber<>(); 
    gistObservable.subscribe(gistTestSubscriber); 
    Gist detailGist = gistTestSubscriber.getOnNextEvents().get(0);
    assertEquals(detailGist.getDescription(), CREATE_DESCRIPTION); 
}

We’ve set it up to run MOCK_ONLY. It’s going to try to find a mock. If it finds a mock, it will return that mock, but it’s never going to hit the network, and that’s an important safety step because if it hits the network and gets a good response, it’s never going to let you know that something went wrong and this is a call that has side effects that you didn’t want to be happening every time you build, so it’s going to be silently doing the thing you didn’t want every time you build.

This is now going to safely, one time, create the Gist. It’s going to read from an asset where I put the Gist that I’m ready to send out. It’s going to send it to GitHub. And then it’s going to try to retrieve it and it’s going to make sure that what it retrieves has the description that it put on it. And that should be enough to establish that the call succeeded, it created the Gist.

We’re not going to check and make sure that this whole file is the file that it put there. We want to test enough to make sure that the thing we wanted to happen happened. Gradle is building. My test passed. I vastly prefer this to clicking through, trying to get a reservation ready to submit.

Here is an example of how long it takes to make a change. I’m going to run this one. That should have been it. Sometimes I forget that was it making a change. But now I don’t have internet access. That’s all the time it takes to recompile after a change. I strongly endorse it.

Next Up: Android Architecture Components and Realm

General link arrow white

About the content

This talk was delivered live in July 2017 at 360 AnDev. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Chris Koeberle

Chris Koeberle has been leading teams developing Android apps for travel sector clients for most of his time at Bottle Rocket. Prior to that, he worked as a Windows, DOS, and embedded systems developer, at Intuit and Lucent Technologies. He took a brief detour into a career as an attorney, but ultimately realized that he was both more passionate and more competent at software development.

4 design patterns for a RESTless mobile integration »

close