cropped-button Introduction

Writing code is always fun. Seeing written code that is actually working is much more fun. I like to see that the process that I wrote gives me expecting result.
I like to test happy path and use my component as soon as possible.

Back in the days I used to treat my application gently…

But we shouldn’t treat our applications gently.
We should crush them, beat them, make them suffer.
We should put them into test harness, always expecting the worst.

cropped-button Test harness

We should test our application. That is obvious. We should put our application components into unit testing, integration testing and automated testing,
and test them under varying conditions. This is called test harness.

Back in the days I used to treat my application gently. I wrote tests, but most of them tested only happy path of the component behavior (or eventually parameters state). I didn’t think much about failure cases. I didn’t jab my components from many perspectives.

This works well for simpler components.

My mind was changed when I read Release It!: Design and Deploy Production-Ready Software by Michael T. Nygard. He wrote that
almost every non trivial application has integration points with different systems. These integration points are the source of many potential application failures, which can harm your application.

This sentence is not only about integration points. I think that every application component has a big list of potential bugs and unexpected failures.

So … How we can be prepared for such failures?

We should treat our components really bad.

Instead of testing happy paths only, we have to test every failure path of our component. We need to think what can go wrong during component usage. We need to be ready to handle every failure of component. This will make our application parts safer.

cropped-button Train schedule API

I’ve recently wrote a simple library, that obtains WKD (Warszawska Kolej Dojazdowa) train schedules (from station A to station B). My intention was to use this library in Android application which will show me next transits without need to type usually used stations, and departure time.

Since WKD does not provide endpoints API, I’ve studied requests and responses from WKD main page, and I’ve found single endpoint that takes some params, and return response in JSON format.
Then I wrote simple library, which has component to hit that endpoint and return obtained JSON as String.

Let me introduce you component responsible for making requests to remote server. This is the first version of WkdHttpClient:

@RequiredArgsConstructor
class WkdHttpClient {

    private final String wkdServerEndpointURL;

    public String fetchTransitsJson(WkdHttpParams params) throws UnirestException {
        HttpResponse response = Unirest.post(wkdServerEndpointURL)
                .headers(headers())
                .fields(fields(params))
                .asJson();

        return response.getBody().toString();
    }

    private Map headers() {
        Map headers = new HashMap();
        headers.put("Content-Type", "application/x-www-form-urlencoded");
        return headers;
    }

    private Map fields(WkdHttpParams params) {
        Map fields = new HashMap();
        fields.put("base", params.getStartStation());
        fields.put("target", params.getDestinationStation());
        fields.put("date", params.getDate());
        fields.put("from", params.getTimeFrom());
        fields.put("to", params.getTimeTo());
        return fields;
    }

    @Builder
    @Value
    static class WkdHttpParams {

        private final String startStation;
        private final String destinationStation;
        private final String date;
        private final String timeFrom;
        private final String timeTo;

    }
}

This component takes given WkdHttpParams value object and make POST request to remote server. When response is obtained, then JSON value is returned as String. This JSON value is being handled further in library.

cropped-button Write tests

To create our tests we will use JUnit4, AssertJ and WireMock. We will be testing WkdHttpClient.

First, let’s create simple happy path test. When we call /schedule endpoint with valid params, then we should get valid JSON response.

public class WkdHttpClientTest {

    @ClassRule
    public static WireMockClassRule wkdEndpoint = new WireMockClassRule(9999);

    WkdHttpClient client;

    String ENDPOINT_PATH = "/schedule";
    String ENDPOINT_URL = "http://localhost:9999" + ENDPOINT_PATH;

    @Before
    public void setUp() {
        client = new WkdHttpClient(ENDPOINT_URL);
    }

    @Test
    public void shouldReturnJsonWhenResponseIsOk() throws Exception {
        String JSON = "{\"schedule\":\"OK\"}";
        stubFor(post(urlEqualTo(ENDPOINT_PATH))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withHeader("Content-Type", "text/html")
                        .withBody(JSON)
                ));

        assertThat(client.fetchTransitsJson(params())).isEqualTo(JSON);
    }

    private WkdHttpParams params() {
        return builder()
                .startStation(KAZIMIEROWKA.getStationName())
                .destinationStation(OPACZ.getStationName())
                .date("2018-02-11")
                .timeFrom("12:00")
                .build();
    }  
}

This test is passing. So correct behavior is covered by test. Now let’s consider worse scenarios…

cropped-button Let’s break the toy!

Are we equipped with hammer? We need one to hit our application. Let’s start with the smallest one.
Our first hammer can be null value passed as WkdHttpParams. Let’s hit very hard…

# HAMMER NO. 1

@Test(expected = NullPointerException.class)
public void shouldThrowNPEWhenParamsPassedAreNull() throws Exception {
    client.fetchTransitsJson(null);
}

Surprisingly, test passed. This happens because fields(WkdHttpParams params) method try to convert given object into Map and fails throwing NullPointerException.

However we should add preconditions for input WkdHttpParams params earlier to validate input as soon as possible.

Let’s add Guava Preconditions at the beginning of the WkdHttpClient fetchTransitsJson method.

  public String fetchTransitsJson(WkdHttpParams params) throws UnirestException {
        Preconditions.checkNotNull(params, "Given WkdHttpParams can not be null!");
        
        HttpResponse response = Unirest.post(wkdServerEndpointURL)
                .headers(headers())
                .fields(fields(params))
                .asJson();

        return response.getBody().toString();
    }

Better, much better. Now params are validated as soon as possible with meaningful error message.

# HAMMER NO. 2

What happens if our HttpWkdClient will be created with invalid endpoint URL? Let’s assume that someone who configured config file with URL endpoint made a simple typo.
Since endpoint is defined as String library won’t notice broken URL until first HTTP call to endpoint! From the user point of view this is unacceptable.

We can fix this by defining endpoint type as URL. We can also check if passed URL is null during creation by Preconditions.

WkdHttpClient(URL wkdServerEndpointURL) {
    Preconditions.checkNotNull(wkdServerEndpointURL, "Error during cosntruction of WkdHttpClient. URL can not be null!");
    this.wkdServerEndpointURL = wkdServerEndpointURL;
}
public String fetchTransitsJson(WkdHttpParams params) throws UnirestException {
   Preconditions.checkNotNull(params, "Given WkdHttpParams can not be null!");

   HttpResponse response = Unirest.post(wkdServerEndpointURL.toString())
                .headers(headers())
                .fields(fields(params))
                .asJson();

   return response.getBody().toString();
}

Let’s hit WkdHttpClient to check if it is already resistant for missing or broken URL.

@Test(expected = MalformedURLException.class)
public void shouldThrowExceptionWhenUrlIsInvalid() throws Exception {
    client = new WkdHttpClient(new URL("invalidURL"));
}

@Test(expected = NullPointerException.class)
public void shouldThrowNPEWhenUrlIsNull() throws Exception {
    client = new WkdHttpClient(null);
}

Works like charm! Object creation is now safe and now we have invariants, that every time WkdHttpClient object is created, then it has valid URL.

# HAMMER NO. 3

What happens if we get 400 or 500 response error from server? Endpoint can change over time, or other server things can happen. Since this is simple library, we should handle that kind of errors by rethrowing them as WkdAPI errors. This will give the library clients simple information: something happen, you can deal with that. If endpoint will change, they can give us feedback, and we can publish new API version with new endpoint (for example).

To do that, I need to create RuntimeException for WkdAPI.

package szczepanski.gerard.wkdapi.core;

public class WkdApiException extends RuntimeException {

    public WkdApiException(String message, Throwable cause) {
        super(message, cause);
    }
}

Now we can use this WkdApiException in WkdHttpClient:

  public String fetchTransitsJson(WkdHttpParams params) {
        Preconditions.checkNotNull(params, "Given WkdHttpParams can not be null!");

        try {
            return fetchTransitsJsonFromServer(params);
        } catch (Exception e) {
            throw new WkdApiException("Error with fetching data from WKD server", e);
        }
    }

    private String fetchTransitsJsonFromServer(WkdHttpParams params) throws UnirestException {
        HttpResponse response = Unirest.post(wkdServerEndpointURL.toString())
                .headers(headers())
                .fields(fields(params))
                .asJson();

        return response.getBody().toString();
    }

Let’s test it. Lets use hammer.

@Test(expected = WkdApiException.class)
    public void shouldThrowWkdApiExceptionWhenServerResponseWith500() {
        stubFor(post(urlEqualTo(ENDPOINT_PATH))
                .willReturn(aResponse()
                        .withStatus(500)
                        .withBody("Internal server error")
                        .withHeader("Content-Type", "text/html")
                ));

        client.fetchTransitsJson(params());
    }

Tests passed. We are prepared for error responses by rethrowing errors for library users.

# HAMMER NO. 4

I think that we should check how WkdHttpClient deals with connection timeout. Integration points sometimes can be not available. They can also respond very slow.
I think that WkdAPI should deal with long connection by breaking it after some time is passed.

Let’s set Unirest conenction timeout to 1000 ms and socket timeout set to 2000 ms. This can be done in WkdHttpClient constructor with static Unirest method setTimeouts.

WkdHttpClient(URL wkdServerEndpointURL) {
    Preconditions.checkNotNull(wkdServerEndpointURL, "Error during cosntruction of WkdHttpClient. URL can not be null!");

    this.wkdServerEndpointURL = wkdServerEndpointURL;
    Unirest.setTimeouts(1000, 2000);
}

Ok, let’s check whether this is gonna work. We will mock server response delay to 2100 ms. After that time WkdApiException should be thrown.

@Test(expected = WkdApiException.class)
public void shouldThrowWkdApiExceptionOnSocketTimeOut() {
    stubFor(post(urlEqualTo(ENDPOINT_PATH))
            .willReturn(aResponse()
                    .withStatus(200)
                    .withFixedDelay(2100)
            ));

    client.fetchTransitsJson(params());
}

All tests passed! Good work! We’ve hit WkdHttpClient from many sides. Now I have confidence that this component will work as expected.

cropped-button Wait… the border cases!

After I’ve published first version of WkdApi I was quite happy. I started to create CLI application on my Linux machine to quick search for next WKD schedules from my home station.

Screenshot from 2018-02-12 00-57-45
Simple CLI program on my machine that uses wkd-api

After some time I’ve reached strange behavior of my app. I’ve noticed that application crashes from various stations at time between 11:30pm to 00:00am.
The exception was look like this:

szczepanski.gerard.wkdapi.core.WkdApiException: Error with fetching data from WKD server

	at szczepanski.gerard.wkdapi.core.WkdHttpClient.fetchTransitsJson(WkdHttpClient.java:32)
	at szczepanski.gerard.wkdapi.core.WkdHttpClientIntTest.shouldFetchJsonFromRemote(WkdHttpClientIntTest.java:25)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: com.mashape.unirest.http.exceptions.UnirestException: java.lang.RuntimeException: java.lang.RuntimeException: org.json.JSONException: A JSONArray text must start with '[' at 1 [character 2 line 1]
	at com.mashape.unirest.http.HttpClientHelper.request(HttpClientHelper.java:143)
	at com.mashape.unirest.request.BaseRequest.asJson(BaseRequest.java:68)
	at szczepanski.gerard.wkdapi.core.WkdHttpClient.fetchTransitsJsonFromServer(WkdHttpClient.java:40)
	at szczepanski.gerard.wkdapi.core.WkdHttpClient.fetchTransitsJson(WkdHttpClient.java:30)
	... 23 more
Caused by: java.lang.RuntimeException: java.lang.RuntimeException: org.json.JSONException: A JSONArray text must start with '[' at 1 [character 2 line 1]
	at com.mashape.unirest.http.HttpResponse.(HttpResponse.java:106)
	at com.mashape.unirest.http.HttpClientHelper.request(HttpClientHelper.java:139)
	... 26 more

Quick endpoint testing and I found the error. Endpoint returns JSON if there is at least one transit from given start station and date. In other case it returns some HTML document!

For example, the latest train from Warszawa Srodmiescie WKD starts at 11:50pm. There are no more trains for given day. Next train is at 0:20am next day.
If I ask WKD server about transits from Warszawa Srodmiescie between 11:51pm and 0:00am I will receive HTML page.
We should handle this border case (the last half hour during the day).

Unirest throws Exception expecting valid JSON. Instead, we obtain HTML document. We should fix this by expecting plain String value from Unirest and the interpret this value (whether it is JSON or not).

To do that let’s reorganize WkdHttpClient. First of all, replace asJson() builder method with asString().

private String fetchTransitsResponseFromServer(WkdHttpParams params) throws UnirestException {
        HttpResponse response = Unirest.post(wkdServerEndpointURL.toString())
                .headers(headers())
                .fields(fields(params))
                .asString();

        return response.getBody().toString();
    }

Then, create inner class inside WkdCHttpClient. Let’s call it WkdServerResponseBody. This class will be representing server response with ability to determine whether server response is valid JSON or not.

    @Value
    @RequiredArgsConstructor(staticName = "of")
    static class WkdServerResponseBody {

        private final String response;

        boolean isValidJson() {
            if (response == null) {
                return false;
            }

            return determineIsJson();
        }

        private boolean determineIsJson() {
            try {
                new Gson().fromJson(response, Object.class);
                return true;
            } catch (com.google.gson.JsonSyntaxException ex) {
                return false;
            }
        }
    }

Last thing to do is to return not plain String, but WkdServerResponseBody class instance.
Clients of WkdHttpClient will recieve object, that can tell them, whether it represents valid JSON or not.

    public WkdServerResponseBody fetchWkdTransitsFromServer(WkdHttpParams params) {
        checkNotNull(params, "Given WkdHttpParams can not be null!");

        try {
            return WkdServerResponseBody.of(fetchTransitsResponseFromServer(params));
        } catch (UnirestException e) {
            throw new WkdApiException("Error with fetching data from WKD server", e);
        }
    }

Now it is time to write unit test that will handle border case scenario. Let’s imagine that we want to retrieve transits from Warszawa Srodmiescie at 11:55 pm.

    @Test
    public void shouldReturnResponseWithNotValidJsonWhenBodyReturnedFromServerIsHtml() {
        stubFor(post(urlEqualTo(ENDPOINT_PATH))
                .willReturn(aResponse()
                        .withStatus(200)
                        .withBody("No transits buddy")
                        .withHeader("Content-Type", "text/html")
                ));

        assertThat(client.fetchWkdTransitsFromServer(params()).isValidJson()).isFalse();
    }

Some code modifications and all tests passed! We’ve handled border case from WKD endpoint!

Our final state:
WkdHttpClient
WktHttpClientTest

cropped-button Conclusion

Fails occur. And that is normal even in trivial applications.

We’ve started from seemingly simple WkdHttpClient, which makes only one call to remote server. As we can see, behind the facade of simple task, there were a lot of demons waiting to shown at some time.

Since we’ve started to think about failures and smash our component with test hammer, we’ve found that demons one by one and simply make our component prepared for some failures.

Think like testers. Do not cuddle your applications.