Testing HTTP applications with a few ok libraries — part 2

In the first part of this series, we have written a type-safe HTTP client in Retrofit and leveraged OkHttp to assert the behaviour of our implementation.

Here comes part 2 and we will now focus on a server-side application and demonstrate how we can leverage OkHttp (yet again!) in testing.

Jump straight in

As before, a lot of the testing philosophy shown here is based on the idea of using HTTP as the layer of asserting application behaviour. Even though we don’t have a clue how our application will be implemented later, we can eventually write a test case now:

public class AppTest {
  private final OkHttpClient appClient =
new OkHttpClient.Builder().build();
  @Test
public void conversation_GET() throws IOException, InterruptedException {
    // bootstrap our application running on an embedded server
new ConversationApp().init();
awaitInitialization();
    // prepares an HTTP GET to our web app
final Call appCall = appClient.newCall(new Request.Builder()
.url("http://localhost:4567/conversation?q=test123")
.get()
.build());
// obtains the HTTP response from our app
final Response appResponse = appCall.execute();
    // asserts expected behaviour
assertThat(appResponse.code()).isEqualTo(200);
}
}

What can we tell from reading the test? It looks like our application will have to respond to a “GET /conversation”, take in a query param “q”, and –for the time being–respond with a “HTTP 200 Ok”.

Before moving on, let’s improve the test code a little bit. If we add more tests and we stick with the above code, we’d end writing the code that bootstraps the application over and over again. We can improve this a little bit by using a JUnit test rule.

The implementation of a rule is basically a before() and after() method that get invoked from JUnit when the test runs; we can implement our own rule and use the methods to start/stop our web application. We then add the rule to the test and now it looks like:

public class AppTest {
  @Rule
public SparkAppUnderTest sparkApp =
new SparkAppUnderTest<>(ConversationApp.class);
  private final OkHttpClient appClient =
new OkHttpClient.Builder().build();
  @Test
public void conversation_GET() throws IOException, InterruptedException {
    // prepares an HTTP call to our web app
final Call appCall = appClient.newCall(new Request.Builder()
.url(sparkApp.serverUrl()+"/conversation?q=test123")
.get()
.build());
// obtains the HTTP response from our app
final Response appResponse = appCall.execute();
    // asserts expected behaviour
assertThat(appResponse.code()).isEqualTo(200);
}
}

As you notice by the naming, we are writing the application with Spark framework since that is flexible and feature-rich enough for our case and it comes with am embedded web server. However, if you take away the basic idea of how we test our application, you are not forced to use Spark. It also could be a Spring Boot app, or Ninja, or Play, or whatever else is fancy enough. Now let’s write our very nice ConversationApp and intentionally fail the test:

public class ConversationApp implements SparkApplication {
  @Override
public void init() {
    get("/conversation", (req, res) -> {
res.status(400);
      return "fooo!";
});
}
}

Testing from one end to the other

Let’s now focus a bit more on the application that we’re building and let’s pretend some things. For instance, our application is unlikely to be deployed in the middle of a desert and is probably going to talk to some sort of service that is another HTTP API. For simplicity, let’s say that we’re talking to the MessagesApi that we built in the first part.

So we slightly adjust our ConversationApp, add the client to the “/conversation” route, and talk to the remote service. We take the query param “q” that we were called with, pass it to MessagesApi, and obtain the response from remote. For now, we just return something (the messge’s text) to our caller.

public class ConversationApp implements SparkApplication {
  @Override
public void init() {
    get("/conversation", (req, res) -> {
final MessagesApi msgApi = new MessagesApi.Builder()
.baseUrl(config("backendUrl", ""))
.build();
      final String keyword = req.queryParams("q");
      final Message msg =
msgApi.findMessage(keyword).execute().body();
      return msg.text;
});
}
}

One small detail to point out here is that we call config() to set the base URL of the remote MessagesApi we talk to. config() is implemented as a rather simple, static method reading a value from a HashMap. If we’re doing this in real world, we probably have some sort of dependency injection in place and we may come to the idea that we’re relying on dependency injection to provide a pre-configured MessagesApi for testing and another one for production.

Guess what? Yes, when we run our test we set that value to somewhere and that somewhere is powered by a MockWebServer:

  @Test
public void conversation_GET() throws IOException, InterruptedException {
    // instructs the mock backend to respond with well-known data
mockBackend.enqueue(
new MockResponse().setBody("{\"text\":\"hello testing!\"}"));
    // configures our application to connect to the mock backend
configSet("backendUrl", mockBackend.url("/").toString());
    // prepares an HTTP call to our web app
final Call appCall = appClient.newCall(new Request.Builder()
.url("http://localhost:4567/conversation?q=test123")
.get()
.build());
// obtains the HTTP response from our app
final Response appResponse = appCall.execute();
    // asserts expected behaviour
assertThat(appResponse.body().source().readUtf8())
.isEqualTo("hello testing!");
}

So what is happening here? Our test executes “GET /conversation?q=test123” to the Spark application, the Spark application reads the configuration value “backendUrl” which points to the MockWebServer, thus executing a HTTP “GET /message” and reading the MockResponse that we’ve scripted in the beginning of the test.

In the way we have written the test the Spark application is now in predictable state. We can exactly tell what content it obtained from MessagesApi and thus we are able to assert for the response that the test expects.

On a personal note, I cannot stress enough the importance of predictable state. There’s rumour around that some software projects end up testing the ‘happy path’ in end-to-end or integration testing because it is not possible to produce predictable state. If you cannot predict the state of your application you are unable to make certain assertions. And if you are unable to make these assertions, it’s more or less likely that you will not be testing for error handling or extreme values (“boundary testing”).

Going ‘feature complete’

At that point, we have everything in place to write more sophisticated tests and more sophisticated features in our application.

Let’s do so by adding some more lines of code. First, a Java class that represents a Conversation:

public class Conversation {
public final Message message;
public final String topic;
  public Conversation(Message message, String topic) {       
this.message = message;
this.topic = topic;
}
}

Then, we alter the Spark application to return an instance of Conversation and transform that response with a so-called ResponseTransformer; in the full example code you can see that we wrote MoshiResponseTranformer by ourselves to convert a Java object to a JSON response body (omitted for reasons of readability in the article).

  get("/conversation", (req, res) -> {
final MessagesApi msgApi = new MessagesApi.Builder()
.baseUrl(config("backendUrl", ""))
.build();

final String keyword = req.queryParams("q");

final Message msg =
msgApi.findMessage(keyword).execute().body();

res.header("Content-Type", "application/json;charset=utf-8");

return new Conversation(msg, "Conversation for " + keyword);
}, MoshiResponseTransformer.create(Conversation.class));

Then, we add more assertions to the test to verify the behaviour of the application. We confirm that the application produced the content that a consumer of the application will expect in real world. Then, we confirm that the application also consumed content from the mock backend.

  @Test
public void conversation_GET() throws IOException, InterruptedException {
// ... mockBackend and appResponse are the same as before
    // asserts that the spark app responds with expected content
assertThat(appResponse.code())
.isEqualTo(200);
assertThat(appResponse.header("Content-Type"))
.contains("application/json");
assertThat(appResponse.body().source().readUtf8())
.isEqualTo("{\"message\":{\"text\":\"hello testing!\"},\"topic\":\"Conversation for test123\"}");
    // asserts that the spark app actually called the mock backend
final RecordedRequest recordedRequest =
mockBackend.takeRequest();
assertThat(recordedRequest.getMethod())
.isEqualTo("GET");
assertThat(recordedRequest.getPath())
.isEqualTo("/message?query=test123");
}

In the first part we spoke a bit about ‘testing philosophy’. So what’s the testing philosophy here?

Again, we use HTTP as the layer for mocking data and asserting behaviour. Why can that be considered a good idea? If we think of consumers and producers we can say that a client will consume content that our application produces. On the other end, we can say that our application consumes content that some other application produced.

When we do testing in such a way we can call that ‘end-to-end testing’.

All tests in one place?

If we want separate test reports from our unit testing (see pt. 1) and our end-to-end testing, we can introduce another source set and custom build tasks in our build.gradle file. That looks something like this:

configurations {
e2eTestCompile.extendsFrom testCompile
e2eTestRuntime.extendsFrom testRuntime
}
sourceSets {
e2eTest {
java {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/test-e2e/java')
}
resources.srcDir file('src/test-e2e/resources')
}
}
task e2eTest(type: Test) {
testClassesDir = sourceSets.e2eTest.output.classesDir
classpath = sourceSets.e2eTest.runtimeClasspath
}
configure(e2eTest) {
group = 'verification'
description = 'Runs end-to-end test cases.'
}
check.dependsOn e2eTest
e2eTest.mustRunAfter test
tasks.withType(Test) {
reports.html.destination = file("${reporting.baseDir}/${name}")
}

The configuration results in three tasks for testing: check, test, and e2eTest. You can now decide to run unit testing only, or end-to-end testing only, or run both of them. Test reports are generated in separate folders and do look somewhat like this:

End-to-end testing report is stored in directory ‘build/reports/e2eTest’
Unit testing report is stored in directory ‘build/reports/test’

Conclusion–tl;dr

Uses OkHttp as a test client to assert HTTP application behaviour.

Uses OkHttp’s MockWebServer to mock data from remote services, thus being able to predict the state of the application under test.

Configures a gradle build to run unit testing and end-to-end testing in separate test runs.


Feedback welcome! What do you think about end-to-end testing? And what is your approach to achieving predictable state?