World-Class Testing Development Pipeline for Android - Part 3


In our previous blog post, “World-Class Testing Development Pipeline for Android - Part 2”, we talked about testing our business logic aka, “the first part of our Testing Development Pipeline”. We discussed how to use the Dependency Inversion Principle as a key principle to test our code and reached the following conclusions:

  • - Test doubles are required to simulate the behaviour of different components and to choose the scope of the test in order to create isolated test environments.
  • - Test doubles can also be used to verify collaboration between components.
  • - The Dependency Inversion Principle will help replace production code easily using test doubles.
  • - Tests based on state and not interaction between components are less coupled to implementation details. That is why it will be way more interesting to write in most of the test scenarios.


In this blog post, we will review several testing approaches that cover the second part of our Testing Development Pipeline under the theme “how to test our integration with a remote service”.

Most of the mobile applications are based on a server side API. This remote service, or API, is used to retrieve information and persist data. Without these services, applications simply can’t work. In 2016, majority of the Android/iOS applications are only a front-end to display information obtained from a service as well as cache or persist some data. However, the integration with an external service is one of the key points in the application development. We need to ensure that our code is sending the right data to the API and correctly parsing the API responses. If your application uses authentication then it is even more important to test the API integration, because the application is useless if the authentication fails.

Testing the integration with a server side API

When dealing with server side API integrations, we need to know if our client’s side code is:

  • - Sending the correct messages to the API.
  • - Parsing the API responses correctly.
  • - Implementing the authentication mechanisms properly.
  • - Handling API errors properly.


In order to validate these 3 keys assumptions, we need to simulate different server side responses and perform assertions over the client side request. To test if the API client is working as expected, we need to use test doubles and perform assertions over the information sent through the network. In this case, we are going to use a test double known as mock and a third party tool known as MockWebServer.

MockWebServer is a scriptable web server for testing HTTP clients implemented by Square and written in Java. The base approach to reach our goal is to enqueue some preconfigured HTTP responses and perform assertions over the HTTP requests sent and the state of the subject under test at the end of the test execution. MockWebServer will start an embedded HTTP server where we can configure our responses. Pointing our API client to the MockWebServer host and configuring some HTTP response we will be able to test our API client. This library will work like a mock, but instead of replacing production code inside the API client, it will replace the production code implemented in server side.

To show the implementation we are going to use some tests extracted from the last API client we implemented some time ago for an e-commerce application. We will focus on three of the most interesting points in this e-commerce API client: the authentication mechanism, the renew session process and the JSON parsing process. The authentication and renew session process is based on a two tokens and email/password system where the API client can renew a token using another non expired token or the user email/password tuple.

public class RenewSessionTest extends ApiClientTest<BaseApiClient> {

  @Test public void shouldKeepTheUserLoggedInAfterRenewSession() {
    performLogin();
    enqueueUnauthorizedResponse();
    enqueueUnauthorizedResponse();
    enqueueSessionRenewedResponse();
    enqueueResponse();

    HttpRequest request = HttpRequest.Builder.to(ANY_ENDPOINT).get();
    getApiClient().send(request, AnyResponse.class);

    assertTrue(getApiClient().isUserLoggedIn());
  }

  @Test public void shouldSendRequestAfterRenewSessionUsingTheNewAccessToken() {
    performLogin();
    enqueueUnauthorizedResponse();
    enqueueSessionRenewedResponse();
    enqueueResponse();

    HttpRequest request = HttpRequest.Builder.to(ANY_ENDPOINT).get();
    getApiClient().send(request, AnyResponse.class);

    assertRequestContainsHeader(ACCESS_TOKEN_KEY, NEW_ACCESS_TOKEN); 
  }
}


In these tests we can see how the subject under test is the API client. Before exercising the API client we are configuring some HTTP responses. We are creating an initial scenario where the user is authenticated in the API client and sending a request to any API endpoint. The server side will then answer with an unauthorized response. This allows us to test the renew session process where the API client should renew sessions with the long-lived token if this is still valid or with the user credentials if the long-lived token has expired. At the end of the test execution we will assert if the user is logged or not or if the request is retried using the new token obtained in the renew session process. An important point to keep in mind is that these tests will fail if there are more requests sent than preconfigured responses.

public class CartApiClientTest extends ApiClientTest<CartApiClient> {

  @Test public void shouldObtainCartFromTheCartEndpoint() {
    enqueueResponse(GET_CART_RESPONSE_FILE);

    CartDTO cart = givenACartApiClient().getCart();

    assertCartContainsExpectedValues(cart);
  }

  @Test public void shouldParseAddToCartResponse() {
    enqueueResponse(ADD_TO_CART_RESPONSE);

    CartDTO cart = givenACartApiClient().addToCart(ANY_SKU_ID);

    assertCartContainsExpectedValuesAfterAddAnItem(cart);
  }

  @Test public void shouldParseUpdateLineResponse() {
    enqueueResponse(UPDATE_CART_LINE_RESPONSE_FILE);

    CartDTO cart = givenACartApiClient().updateLine(ANY_SKU_ID, 3);

    assertCartContainsExpectedValuesAfterLineUpdated(cart);
  }
}


As aforementioned, another interesting point to test in your API client is the JSON parsing. To achieve this, we are going to use more complicated responses based on the same approach. In this test case we are going to preconfigure responses using as the body the contents of a JSON file stored in the test resources directory. During the assert stage, we will check if the object parsed matches with the JSON we have configured as response. These tests check if the process to add or modify an item into a cart system is working correctly.

Another interesting approach is to check whether the information sent to the API is as expected. Using MockWebServer we can get a list of HTTP requests sent and perform assertions. An example could be during the renew session process, as previously highlighted, where we checked whether the request sent to the server contains the expected headers.

Scope

These tests have a large scope. We are testing simultaneously our usage and configuration of the HTTP client implementation, our usage and configuration of the JSON parsing library in addition to the entire logic implemented to perform the authentication process, the renew session mechanism and the error handling. Another valid approach could be based on the intensive usage of the Dependency Inversion Principle. With a testable architecture we could replace our HTTP client implementation with a mock and verify the information sent to the HTTP client. Given the fact that the HTTP client implementation is a mock, we could also configure the HTTP responses similarly to the MockWebServer. To test the JSON parsing we could write solitary tests to check our JSON parsing configuration without exercising the rest of the API client.

Infrastructure

The infrastructure needed is just a third party library like MockWebServer or WireMock and a little base test case or some utility classes to enqueue responses easily and perform assertions over the already sent requests. You can also implement a little class representing the server side API to abstract the usage of a third party library without coupling your tests implementation to the library under consideration.

public abstract class ApiClientTest<T extends BaseApiClient> {

  private MockWebServer server;
  private T apiClient;

  @Before public void setUp() throws Exception {
    this.server = new MockWebServer();
    this.server.start();
    apiClient = initializeApiClient();
  }

  @After public void tearDown() throws IOException {
    server.shutdown();
  }

  protected void enqueueResponse(String fileName) throws IOException {
    MockResponse mockResponse = new MockResponse();
    String fileContent = getContentFromFile(fileName);
    mockResponse.setBody(fileContent);
    server.enqueue(mockResponse);
  }

  protected void enqueueUnauthorizedResponse() {
    MockResponse mockResponse = new MockResponse();

    mockResponse.setResponseCode(UNAUTHORIZED_CODE);
    server.enqueue(mockResponse);
  }

  protected void assertNumberOfRequestSent(int numberOfRequests) {
    assertEquals(numberOfRequests, server.getRequestCount());
  }


  protected void assertRequestContainsHeader(String key, String expectedValue, int requestIndex)
      throws InterruptedException {
    RecordedRequest recordedRequest = getRecordedRequestAtIndex(requestIndex);
    String value = recordedRequest.getHeader(key);
    assertEquals(expectedValue, value);
  }


  protected void assertPostRequestSentTo(String url) throws InterruptedException {
    RecordedRequest request = server.takeRequest();
    assertEquals(url, request.getPath());
    assertEquals(POST_METHOD, request.getMethod());
  }
}


Results

When mocking our server side API we can easily reproduce different scenarios and API behaviours to check if our API client implementation is responding as expected. At the same time, we are getting a live documentation of the server side API based on all the written tests and the HTTP responses. We can also use these test cases to reproduce corner or edge cases where the information obtained from the HTTP responses is incomplete or the network conditions are unfavorable. As we did with our business logic layer tests, we have created an isolated environment where our tests are repeatable, easy to write and well designed.

If you want to review tests related to an API Client implementation you can take a look at Open Source repositories we have written in Java and Swift using this approach.

References


You can read the next post in this series following here:

World-Class Testing Development Pipeline for Android - Part 4

Subscribe to Karumi Blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!