Nowadays, writing test cases for every feature of your app has become inevitable! It ensures the app’s correctness, behaviour, and usability at any given moment. For unit & integration testing, Android supports multiple frameworks.

Recently we were working on an Android application that was full of API calls. Most of them were nested or getting called at the same time.
To achieve maximum code coverage, we decided to write test cases that include API calls. For obvious reasons, it's not a good practice to call the actual API for test cases. After exploring a lot of libraries, we found the MockWebServer library that helps in mocking a real server & writing API test cases. It also handles the uncertain sequence of API calls flawlessly.

So we thought to demonstrate how to set up a mock server using the MockWebServer library and how we overcame the challenge of testing async API calls.

Let's create a demo application, which displays data from two different Star Wars APIs.

To test the displayed data, we wrote a test case with the help of Espresso. After running the test case, it failed, because the text field's data was not matching the expected data as API response keeps changing.
And here comes MockWebServer to the rescue. Instead of dealing with an actual server's response which changes, it's better to deal with a mocked response that can be controlled.

What is MockWebServer?
MockWebServer is a library that takes our request, sets up the mock server, hits the localhost instead of the actual URL & returns the response which we have set. It also tests that your code survives in awkward-to-reproduce situations like 500 errors or slow-loading responses.

Below are the steps to perform API mocking with MockWebServer:
1. Add dependency:
Update build.gradle with

androidTestImplementation 'com.squareup.okhttp3:mockwebserver:$latest_version'

2. Setup the mock server:

  • Create a MockWebServer object
  • Start the server
  • Update the BASE_URL with localhost
  • Add the expected response body in enqueue()
  • Launch the activity
MockWebServer mockWebServer = new MockWebServer();
mockWebServer.start();
ApiUrls.BASE_URL = mockWebServer.url("/").toString();
mockWebServer.enqueue(new MockResponse().setBody(response_data_string));
activityRule.launchActivity(intent);

Now, after running the test, we see it is failing! AGAIN!

If your code has not failed yet, you have not tried anything! So let’s learn & grow from mistakes.

On investigation, we found a few problems.

Problem 1:
We have multiple API calls.

// First API call to get movie details
movieDetailsViewModel.initMovieDetailsAPI();
movieDetailsViewModel.getMovieDetails().observe(this,
movieDetailResponse -> {
	if (movieDetailResponse != null) {
		activityMainBinding.tvMovieDetails
		.setText(movieDetailResponse.getTitle());
        }});


// Second API call to get a movie character details
movieCharacterViewModel.initMovieCharacterDetailsAPI();
movieCharacterViewModel.getMovieCharacterDetails().observe(this, movieCharacterResponses -> {
     if (movieCharacterResponses != null) {
		activityMainBinding.tvCharacterDetails
		.setText(movieCharacterResponses.getName());
        }});

enqueue() returns a response without considering the order of requests. But because of asynchronous API calls, we can't predict its sequence. Hence the enqueue method is not the solution to this.

Solution:
Use a Dispatcher to handle requests based on request paths. Here we can add the condition which checks the API path and accordingly dispatches the response.

final Dispatcher dispatcher = new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
        if (request.getPath().equals("url_path")) {
            //Return the data with response code
        } else if (request.getPath().equals("another_url_path")) {
            //Return the data with response code
        }
        return new MockResponse().setResponseCode(404);
    }
};
mockWebServer.setDispatcher(dispatcher);

Problem 2:
Because of asynchronous calls, assert statements are getting executed before we receive a mocked API response.

Solution:
To stall the execution of assert statements before receiving mocked API response, we need to add some time lag. We can achieve that by using System.sleep() method, but below are a few better solutions:
1. With the help of Espresso Idling Resources.
2. With the help of Awaitility library.

We are using the Awaitility library to handle this asynchronous operation. One of its features is, we can add the waiting time.
For example:
await().atMost(10, SECONDS).until(() -> write_your_waiting_condition);
In the above code, we have specified a maximum waiting time of 10 seconds and once the expected response arrives within the specified time, the remaining code gets executed.

Now run the test & it SUCCEEDS!!! 😁

In the same way, we can also test timeout and valid/invalid scenarios by providing the response code with expected output.

You can find the demo project for this article with Retrofit, Awaitility & MVVM pattern on Github.

We hope this post helped you.
Thank you!

References: