Testing HTTP applications with a few ok libraries

David
8 min readJul 28, 2016

--

Some of us will have heard of Square’s point-of-sales technology. While building these products the company has maintained a GitHub account and kept publishing open-source software.

Credit goes to Jake Wharton and Jesse Wilson who have written a bunch of libraries to work with HTTP APIs in Android. That technology stack is sometimes referred to as “a few ok libraries” which include Okio, OkHttp, Retrofit, and Moshi.

If you ask “why? why should I use these libraries?”, I suggest that you take a quick look at the following part of a presentation at Droidcon Montreal 2015 (relevant part is about 5 minutes in time). Long story short: it’s a memory-efficient way to do buffering.

A few Ok libraries — relevant part begins at 47:35, ends at ~53:00

Beside that, a nice thing about the libraries is that they were intended for writing Android applications. However, their usage isn’t limited to the world of Android and rather can be used in any Java application.

In this series of articles, we’ll take a look at how you can use the so-called ‘ok libraries’ for testing. We will implement a HTTP/JSON client and test our client-side code. In the second part, we will write a server-side HTTP application and see how we can leverage OkHttp for the sake of end-to-end testing. In the third and final part, we will see how to integrate our tests in a build tool and a deployment tool and also talk a little about further steps we could take.

Fetching messages from remote

Nowadays, examples are full of Foo and Bar or even FooBar stuff; so we’ll try to stick with the odd messaging example. Our example API is a rather simple thing:

GET /message?q=keyword
Host: localhost:8888
HTTP/1.1 200 Ok
{"text": "hello world!"}

An HTTP client for that API can be written in Retrofit like this:

public class Message {
public String text;
}
public interface MessagesApi { @GET("/message")
Call<Message> findMessage(@Query("q") String value);
}

The annotations on the interface are rather self-explainatory. Guess what? @GET tells Retrofit to do a HTTP GET, the argument to that is the URL path; @Query on the method parameter translates the given value into an URL query part, e.g. “?q=whatever”.

When we use that interface, retrofit creates a proxy instance of MessagesApi and we then invoke findMessage() on that instance to get a Call. You can imagine the call to be a “one-off” adapter that dispatches the request, obtains the response from the server, and converts the JSON response body to an instance of our Message class.

To make all that work, we still need a few lines of “glueing” code:

Retrofit retrofit = new Retrofit.Builder()
.client(new OkHttpClient.Builder().build())
.addConverterFactory(MoshiConverterFactory.create())
.baseUrl("http://localhost:8888")
.build();
MessagesApi ourApi = retrofit.create(MessagesApi.Class)

What we do here is that we tell Retrofit to use a plain OkHttpClient for HTTP communication, use Moshi to convert request/response bodies with JSON payload to Java objects (and vice versa), and talk to the service at “http://localhost:8888”. Retrofit then does a bit of reflection magic and gives us a pre-configured MessagesApi instance.

Talking to a web server

Now that we’ve got our client implementation in place, we can move to the part where we actually talk to a “real” web server and write the tests that verify our client implementation.

OkHttp comes with scriptable web server that you can use to replay mocked HTTP responses and to inspect the HTTP requests that your client has made. That MockWebServer can be used as a JUnit rule, thus being relatively simple to use. Let’s take a look at a very basic example:

public class MessagesApiTest {  @Rule
public MockWebServer mockBackend = new MockWebServer();
@Test
public void everythingOk() {
MessagesApi msgApi = new Retrofit.Builder()
.client(new OkHttpClient.Builder().build())
.addConverterFactory(MoshiConverterFactory.create())
.baseUrl(mockBackend.url("/").toString())
.build()
.create(MessagesApi.class);
assertThat(msgApi).isNotNull();
}
}

At that point, we just set up the mock web server and set the base URL of our MessagesApi client to the URL of that server. If you run the above example, the test should turn green and you should see some logging output like:

okhttp3.mockwebserver.MockWebServer$3 execute
INFO: MockWebServer[52796] starting to accept connections
okhttp3.mockwebserver.MockWebServer$3 acceptConnections
INFO: MockWebServer[52796] done accepting connections: Socket closed

Time to add some real testing.

Now that we’ve got all the pieces in place, it’s time to add some “real” testing. That’s the time where we’ve got to ask the question: ‘What do we expect the application to do?’

By calling the MessagesApi interface, we expect the client to obtain an instance of a Message entity as a result of calling the web server. So let’s write a test that verifies such behaviour.

  @Test
public void findMessage_obtainsEntityObject() throws IOException {
mockBackend.enqueue(
new MockResponse().setBody("{\"text\":\"hello!\"}")
);
final Response<Message> response =
mockApi()
.findMessage("")
.execute();
final Message entity = response.body();
assertThat(entity.text).isEqualTo("hello!");
}

We can see that MockWebServer has an API to replay responses. We do so by passing a MockResponse to enqueue(). Here, we set the response body to some JSON payload. Then, we use our MessageApi instance (mockApi() is some utility method plugging things together, see above) to execute an HTTP request and read the response’s body.

We expect that the JSON is converted to a Message instance and that its text property actually matches the value of the MockResponse we’ve just scripted. So we assert for that behaviour and enjoy that it says “hello!”.

Second, we expect our client to execute a request that the server understands. A test for that behaviour may look like:

  @Test
public void findMessage_dispatchesExpectedRequest()
throws IOException, InterruptedException {
mockBackend.enqueue(
new MockResponse().setBody("{\"text\":\"hello!\"}"));
mockApi().findMessage("123").execute(); final RecordedRequest recordedRequest =
mockBackend.takeRequest();

assertThat(recordedRequest.getMethod())
.isEqualTo("GET");
assertThat(recordedRequest.getPath())
.isEqualTo("/message?q=123");
}

Again, we replay a scripted response and execute an HTTP request. Then, by calling takeRequest() the MockWebServer API gives us the request that our client has just executed. Let’s remember our tiny API specification: we make sure that the request verb was “GET” and that path (and query) of the URL match the parameters that we’ve passed, here it should be “/message?q=123”.

A little improvement

In our test cases we make sure that our client implementation executes the requests that the server expects and that it obtains the expected Java types from server responses.

When we look at our code now, you may notice one thing. Let’s ask ourselves a question:

If we were on another team that just uses MessagesApi, what is the minimal piece of information we need to know?

Yes, you’ve probably got it. Albeit we wrote about 6 lines of “glue” code in the test cases, we just need the base URL of the remote server.

That we’re plugging together things like a MoshiConverterFactory or an OkHttpClient can be seen as a mere implementation detail; if we omit the MoshiConverterFactory for instance, we’d end up with an error telling that the response body cannot be converted to a Message entity. Also, in theory, we could use something other than Moshi to do the conversion.

Let’s try to improve our client a little bit by adding a helper that takes the base URL as an input and returns a MessagesApi as output. We can do so by writing a builder:

public interface MessagesApi {  @GET("message")
Call<Message> findMessage(@Query("q") String keyword);
class Builder {
private String baseUrl;
public Builder() {} public Builder baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
public MessagesApi build() {
return new Retrofit.Builder()
.client(new OkHttpClient.Builder().build())
.addConverterFactory(MoshiConverterFactory.create())
.baseUrl(baseUrl)
.build()
.create(MessagesApi.class);
}
}
}

Yes, we’ve produced almost 3-times the lines of code than before, but we can now use MessagesApi in the same way like the other ‘ok APIs’ and it takes away the burden of providing OkHttpClient and MoshiConverterFactory.

Let’s add a test case that also demonstrates how a consumer of our client should use it:

  @Test
public void builder_returnsInstance() {
final MessagesApi messagesApi = new MessagesApi.Builder()
.baseUrl("http://localhost:8888")
.build();
assertThat(messagesApi).isNotNull();
}

If you dislike the idea of exchanging 6 lines of code vs. 16 lines of code, you may also think of writing some static utility method. On the other hand, having a builder class is maybe a little bit more in line with the APIs of the ‘ok libraries’.

Testing philosophy

After we have seen ‘what to test’, we should also ask the questions ‘how?’ and ‘why?’.

How did we write the tests? We have a mock web server scripted with mocked responses. From looking at the tests, we can tell that the intention of the client is to speak to an HTTP/JSON API with the specified behaviour.

Why did we write the tests like that? Basically, we introduce mock data at the HTTP layer in order to mimic behaviour of a real-world service that our client will connect to. We also verify behaviour at the HTTP layer.

If you come to the conclusion that such a style of testing is more of a integration test, you may (or may not) have a good point. It comes down to the way we think about it.

If we think of our code in the way of a “composition of libraries” that we mix together and we think of the tests in a way of making sure things fit to each other, then yes, we can call it an integration test.

Let’s again pretend we are not writing the client but rather are using (0r reading) its code. Let’s also pretend we’re working on a large-scale project, maybe with different people, who have different levels of knowledge, who are working in different physical locations, and all these kind of things we experience in software projects.

In that scenario, a client is probably just one part of a rather complex application. So if we’re looking at it from that perspective, we can think of the whole client implementation as some kind of a “unit” of a larger software project.

Furthermore, if we’re an alien from outer space (or just an ordinary co-worker that joined the project team a few days ago) and we look at the code, we’d like to have two things: traceability and maintainability.

Traceability should answer the question “why was this written to be like that?” and “what was the intention behind that?”.

Maintainability should help us refactoring and improving the code without breaking it. If we ask the question “is it possible to change this to be like that?” and the answer is “yes”, then we probably have maintainable code.

Did we achieve that? Personally, I think yes, at least to some degree.

One example for traceability is: the field “text” of the Message entity was obviously given that name in order to match the JSON property “text”. The intention behind that was to have a Java class representing the JSON produced by the server.

An example for maintainability is: can we change the name of the member “text”? If we do so and re-run our tests, we will see that they result in an error. So we cannot do so — at least not without taking some further action. If we really want to alter the field name, maybe we should use Moshi’s @Json annotation; whether that’s a good or not-so-good idea in the first place remains another question.

Summary — tl;dr

Retrofit, OkHttp, Okio and Moshi are a nice fit for writing type-safe HTTP clients in Java.

It’s not limited to Android. You can use it in any Java application.

We test our client-side implementation by scripting OkHttp’s MockWebServer so that it mimics real-world API specifications.

Tests assert that

  • the client executes requests that the server expects, and
  • the client uses Java types for content produced by the server.

Code example is on GitHub.

Feedback welcome!

What do you think? Do you like or disagree with that style of testing? How do you write tests for client-side code?

--

--