Categories
android

Retrofit 2 – Mocking HTTP Responses

In the previous post, I discussed implementing a custom Retrofit Client to mock out different HTTP response codes. This mechanism works well for Retrofit versions 1.9 and below but has its drawbacks. After trying out Retrofit 2, I have adjusted the previous sample and managed to achieve the same results (with some improvements 😀).

In order to test your Android apps, one thing that normally gets frequently overlooked is the apps ability to handle different server responses. What if your server goes down for a while? Does your app fall over with it – or does it gracefully recover? Things like this are difficult to emulate with real servers, which is why mocking responses is such a great way to ensure your app is awesome.

In this example, we will look at creating an app that retrieves a quote of the day from a web service and displays it to the user. We will also add a failure mechanism to the front end to show the user a retry button if something goes wrong. We will also look at testing these failure mechanisms.

Example 1:

  1. Create a Rest Service interface that will be used with Retrofit.
    public interface QuoteOfTheDayRestService {
    
        @GET("/qod.json")
        Call<QuoteOfTheDayResponse> getQuoteOfTheDay();
    
    }
  2. Ensure your activity calls the Retrofit Service that you have just created. In the code below, the service gets created and the activity asynchronously calls getQuoteOfTheDay(). When a successful response is received from the server, the quote is displayed otherwise a retry button is shown with an error message.
  3. import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    
    import java.io.IOException;
    import java.lang.annotation.Annotation;
    
    import okhttp3.OkHttpClient;
    import okhttp3.ResponseBody;
    import retrofit2.Call;
    import retrofit2.Callback;
    import retrofit2.Converter;
    import retrofit2.Response;
    import retrofit2.Retrofit;
    import retrofit2.converter.jackson.JacksonConverterFactory;
    import za.co.riggaroo.retrofittestexample.interceptor.LoggingInterceptor;
    import za.co.riggaroo.retrofittestexample.pojo.QuoteOfTheDayErrorResponse;
    import za.co.riggaroo.retrofittestexample.pojo.QuoteOfTheDayResponse;
    
    public class MainActivity extends AppCompatActivity {
    
        private TextView textViewQuoteOfTheDay;
        private Button buttonRetry;
    
        private static final String TAG = "MainActivity";
        private QuoteOfTheDayRestService service;
        private Retrofit retrofit;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            textViewQuoteOfTheDay = (TextView) findViewById(R.id.text_view_quote);
            buttonRetry = (Button) findViewById(R.id.button_retry);
            buttonRetry.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    getQuoteOfTheDay();
                }
            });
    
            OkHttpClient client = new OkHttpClient();
            // client.interceptors().add(new LoggingInterceptor());
            retrofit = new Retrofit.Builder()
                    .baseUrl(QuoteOfTheDayConstants.BASE_URL)
                    .addConverterFactory(JacksonConverterFactory.create())
                    .client(client)
                    .build();
            service = retrofit.create(QuoteOfTheDayRestService.class);
            getQuoteOfTheDay();
    
        }
    
    
        private void getQuoteOfTheDay() {
            Call<QuoteOfTheDayResponse> call =
                    service.getQuoteOfTheDay();
    
            call.enqueue(new Callback<QuoteOfTheDayResponse>() {
    
                @Override
                public void onResponse(Call<QuoteOfTheDayResponse> call, Response<QuoteOfTheDayResponse> response) {
                    if (response.isSuccessful()) {
                        textViewQuoteOfTheDay.setText(response.body().getContents().getQuotes().get(0).getQuote());
                    } else {
                        try {
                            Converter<ResponseBody, QuoteOfTheDayErrorResponse> errorConverter = retrofit.responseBodyConverter(QuoteOfTheDayErrorResponse.class, new Annotation[0]);
                            QuoteOfTheDayErrorResponse error = errorConverter.convert(response.errorBody());
                            showRetry(error.getError().getMessage());
    
                        } catch (IOException e) {
                            Log.e(TAG, "IOException parsing error:", e);
                        }
    
                    }
                }
    
                @Override
                public void onFailure(Call<QuoteOfTheDayResponse> call, Throwable t) {
                    //Transport level errors such as no internet etc.
                }
            });
    
    
        }
    
        private void showRetry(String error) {
            textViewQuoteOfTheDay.setText(error);
            buttonRetry.setVisibility(View.VISIBLE);
    
        }
    }
  4. You might wonder, how do we test that the retry button is properly shown when the server is down without turning the server off? 😁 Using Espresso and Retrofit MockWebServer we can easily achieve this. Create mock JSON and store it in your androidTest folder.
    Success Response Sample JSON:

    {
      "success": {
        "total": 1
      },
      "contents": {
        "quotes": [
          {
            "quote": "I came from a real tough neighborhood. Once a guy pulled a knife on me. I knew he wasn't a professional, the knife had butter on it.",
            "length": "132",
            "author": "Rodney Dangerfield",
            "tags": [
              "funny",
              "humor"
            ],
            "category": "funny",
            "id": "3e_ZsKxPKu5SuuAa6Pa_0AeF"
          }
        ]
      }
    }
    

    404 Not Found Sample JSON:

    {
      "error": {
        "code": 404,
        "message": "Quote Not found"
      }
    }
  5. Below is the sample for testing the positive case (a quote is displayed to the user) and the negative case. A retry button is displayed to the user when the server is returning an error. You might want to do some other logic when there is an error but for simplicity, we will just display a retry button.
    import android.content.Intent;
    import android.support.test.InstrumentationRegistry;
    import android.support.test.espresso.matcher.ViewMatchers;
    import android.support.test.rule.ActivityTestRule;
    import android.support.test.runner.AndroidJUnit4;
    import android.test.InstrumentationTestCase;
    import com.squareup.okhttp.mockwebserver.MockResponse;
    import com.squareup.okhttp.mockwebserver.MockWebServer;
    
    import org.junit.After;
    import org.junit.Before;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    
    import static android.support.test.espresso.Espresso.onView;
    import static android.support.test.espresso.assertion.ViewAssertions.*;
    import static android.support.test.espresso.matcher.ViewMatchers.*;
    import static android.support.test.espresso.matcher.ViewMatchers.withId;
    import static android.support.test.espresso.matcher.ViewMatchers.withText;
    
    /**
     * @author rebeccafranks
     * @since 15/10/25.
     */
    @RunWith(AndroidJUnit4.class)
    public class MainActivityTest extends InstrumentationTestCase {
    
    
        @Rule
        public ActivityTestRule<MainActivity> mActivityRule =
                new ActivityTestRule<>(MainActivity.class, true, false);
        private MockWebServer server;
    
        @Before
        public void setUp() throws Exception {
            super.setUp();
            server = new MockWebServer();
            server.start();
            injectInstrumentation(InstrumentationRegistry.getInstrumentation());
            QuoteOfTheDayConstants.BASE_URL = server.url("/").toString();
        }
    
        @Test
        public void testQuoteIsShown() throws Exception {
            String fileName = "quote_200_ok_response.json";
            server.enqueue(new MockResponse()
                    .setResponseCode(200)
                    .setBody(RestServiceTestHelper.getStringFromFile(getInstrumentation().getContext(), fileName)));
    
            Intent intent = new Intent();
            mActivityRule.launchActivity(intent);
    
            onView(withId(R.id.button_retry)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)));
            onView(withText("I came from a real tough neighborhood. Once a guy pulled a knife on me. I knew he wasn't a professional, the knife had butter on it.")).check(matches(isDisplayed()));
        }
    
    
        @Test
        public void testRetryButtonShowsWhenError() throws Exception {
            String fileName = "quote_404_not_found.json";
    
            server.enqueue(new MockResponse()
                    .setResponseCode(404)
                    .setBody(RestServiceTestHelper.getStringFromFile(getInstrumentation().getContext(), fileName)));
    
            Intent intent = new Intent();
            mActivityRule.launchActivity(intent);
    
            onView(withId(R.id.button_retry)).check(matches(isDisplayed()));
            onView(withText("Quote Not found")).check(matches(isDisplayed()));
        }
    
        @After
        public void tearDown() throws Exception {
            server.shutdown();
        }
    
    }

    From the sample above, we can see that the method setUp() is starting up the MockWebServer and setting the BASE_URL  of the entire app to point to the local mock server’s URL.
    The test testQuoteIsShown()  is enqueuing a 200 OK response on the mock server, with the JSON from the file we defined previously as the body. We then launch the Activity. Using Espresso, we ensure that the retry button is hidden and the quote is displayed.
    The test testRetryButtonShowsWhenError() does a similar thing, except it queues up a 404 response, ensures that the retry button is shown and that the text “Quote Not Found” is displayed.

  6. As you can see from the above sample, we have achieved the same results as the previous post, but using Retrofit 2.

Pros of testing this way:

  • Really simple to test different HTTP Status Codes
  • Not much code needed to emulate the different responses

Cons of testing this way:

  • Difficult to dynamically create the responses

Example 2:

In the sample app, I have also included another way to test your Rest API by implementing the interface defined at the very beginning.

  1. Create a mock implementation of your API methods. In this stub, we create a dummy quote and return that quote every time. This is defined in the androidTest folder.
    import java.util.ArrayList;
    
    import retrofit2.Call;
    import retrofit2.Response;
    import retrofit2.mock.BehaviorDelegate;
    import za.co.riggaroo.retrofittestexample.pojo.Contents;
    import za.co.riggaroo.retrofittestexample.pojo.Quote;
    import za.co.riggaroo.retrofittestexample.pojo.QuoteOfTheDayResponse;
    
    /**
     * @author rebeccafranks
     * @since 15/10/24.
     */
    public class MockQuoteOfTheDayService implements QuoteOfTheDayRestService {
    
        private final BehaviorDelegate<QuoteOfTheDayRestService> delegate;
    
        public MockQuoteOfTheDayService(BehaviorDelegate<QuoteOfTheDayRestService> service) {
            this.delegate = service;
        }
    
        @Override
        public Call<QuoteOfTheDayResponse> getQuoteOfTheDay() {
            QuoteOfTheDayResponse quoteOfTheDayResponse = new QuoteOfTheDayResponse();
            Contents contents = new Contents();
            Quote quote = new Quote();
            quote.setQuote("Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.");
            ArrayList<Quote> quotes = new ArrayList<>();
            quotes.add(quote);
            contents.setQuotes(quotes);
            quoteOfTheDayResponse.setContents(contents);
            return delegate.returningResponse(quoteOfTheDayResponse).getQuoteOfTheDay();
        }
    }

    2. Create another mock implementation that will return the error scenario.

mport android.util.Log;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

import java.io.IOException;

import okhttp3.MediaType;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.mock.Calls;
import za.co.riggaroo.retrofittestexample.pojo.Error;
import za.co.riggaroo.retrofittestexample.pojo.QuoteOfTheDayErrorResponse;
import za.co.riggaroo.retrofittestexample.pojo.QuoteOfTheDayResponse;

import retrofit2.mock.BehaviorDelegate;
/**
 * @author rebeccafranks
 * @since 15/10/25.
 */
public class MockFailedQODService implements QuoteOfTheDayRestService {
    private static final String TAG = "MockFailedQOD";
    private final BehaviorDelegate<QuoteOfTheDayRestService> delegate;

    public MockFailedQODService(BehaviorDelegate<QuoteOfTheDayRestService> restServiceBehaviorDelegate) {
        this.delegate = restServiceBehaviorDelegate;

    }

    @Override
    public Call<QuoteOfTheDayResponse> getQuoteOfTheDay() {
        za.co.riggaroo.retrofittestexample.pojo.Error error = new Error();
        error.setCode(404);
        error.setMessage("Quote Not Found");
        QuoteOfTheDayErrorResponse quoteOfTheDayErrorResponse = new QuoteOfTheDayErrorResponse();
        quoteOfTheDayErrorResponse.setError(error);

        ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
        String json = "";
        try {
            json = ow.writeValueAsString(quoteOfTheDayErrorResponse);
            Response response = Response.error(404, ResponseBody.create(MediaType.parse("application/json") ,json));
            return delegate.returning(Calls.response(response)).getQuoteOfTheDay();
        } catch (JsonProcessingException e) {
            Log.e(TAG, "JSON Processing exception:",e);
            return Calls.failure(e);
        }

    }
}
  1. Create a test which will test the API response parsing.
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.SmallTest;


import junit.framework.Assert;

import java.lang.annotation.Annotation;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Converter;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.mock.BehaviorDelegate;
import retrofit2.mock.MockRetrofit;
import retrofit2.mock.NetworkBehavior;
import za.co.riggaroo.retrofittestexample.pojo.QuoteOfTheDayErrorResponse;
import za.co.riggaroo.retrofittestexample.pojo.QuoteOfTheDayResponse;

/**
 * @author rebeccafranks
 * @since 15/10/23.
 */
public class QuoteOfTheDayMockAdapterTest extends InstrumentationTestCase {
    private MockRetrofit mockRetrofit;
    private Retrofit retrofit;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        retrofit = new Retrofit.Builder().baseUrl("http://test.com")
                .client(new OkHttpClient())
                .addConverterFactory(JacksonConverterFactory.create())
                .build();

        NetworkBehavior behavior = NetworkBehavior.create();

        mockRetrofit = new MockRetrofit.Builder(retrofit)
                .networkBehavior(behavior)
                .build();
    }


    @SmallTest
    public void testRandomQuoteRetrieval() throws Exception {
        BehaviorDelegate<QuoteOfTheDayRestService> delegate = mockRetrofit.create(QuoteOfTheDayRestService.class);
        QuoteOfTheDayRestService mockQodService = new MockQuoteOfTheDayService(delegate);


        //Actual Test
        Call<QuoteOfTheDayResponse> quote = mockQodService.getQuoteOfTheDay();
        Response<QuoteOfTheDayResponse> quoteOfTheDayResponse = quote.execute();

        //Asserting response
        Assert.assertTrue(quoteOfTheDayResponse.isSuccessful());
        Assert.assertEquals("Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.", quoteOfTheDayResponse.body().getContents().getQuotes().get(0).getQuote());

    }

    @SmallTest
    public void testFailedQuoteRetrieval() throws Exception {
        BehaviorDelegate<QuoteOfTheDayRestService> delegate = mockRetrofit.create(QuoteOfTheDayRestService.class);
        MockFailedQODService mockQodService = new MockFailedQODService(delegate);

        //Actual Test
        Call<QuoteOfTheDayResponse> quote = mockQodService.getQuoteOfTheDay();
        Response<QuoteOfTheDayResponse> quoteOfTheDayResponse = quote.execute();
        Assert.assertFalse(quoteOfTheDayResponse.isSuccessful());

        Converter<ResponseBody, QuoteOfTheDayErrorResponse> errorConverter = retrofit.responseBodyConverter(QuoteOfTheDayErrorResponse.class, new Annotation[0]);
        QuoteOfTheDayErrorResponse error = errorConverter.convert(quoteOfTheDayResponse.errorBody());

        //Asserting response
        Assert.assertEquals(404, quoteOfTheDayResponse.code());
        Assert.assertEquals("Quote Not Found", error.getError().getMessage());

    }
}

As you can see, the implementation that we mocked out previously is now being used in the test. In the setUp()  method the MockRetrofit object is created. This MockRetrofit object wraps around the dummy implementation that we created and emulates a network call by adding delays to the calls.

The testRandomQuoteRetrieval()  uses the first mock implementation to test the positive scenarios, whereas testFailedQuoteRetrieval()  uses the second mock implementation which will return a 404 error.

This can be used to test operations on the API and test front-end scenarios such as adding items to the server without sending it off to the server unnecessarily.

Pros of testing this way:

  • Can create rich data to return to the tests which indicates testing can be more meaningful
  • Easy enough to set up
  • JSON isn’t static

Cons:

  • Have to create new API implementations for different error scenarios
  • Hard to see from a glance what the API returns

I think a combination of the two mechanisms described above can definitely cover most scenarios that you would need to test in order to make your app stable. What are your thoughts?

You can check out the full project here on Github.


24 replies on “Retrofit 2 – Mocking HTTP Responses”

Excellent post! Id like to ask you one quick question about retrofit2: Have you managed to print your raw JSON responses from server in the positive case? I read that it’s possible with retrofit2 but can’t figure out how. The article was really useful and pleasant to read. Thanks.

Hi!
Thanks for the feedback.
Yes I actually had the same thoughts whilst working through this example. It is possible, I have also included some basic logging in my example app on Github. You can find the code here:
https://github.com/spongebobrf/android-retrofit-test-examples/blob/master/RetrofitTestExample/app/src/main/java/za/co/riggaroo/retrofittestexample/interceptor/LoggingInterceptor.java
Obviously you might want to do a bit more logging.

There are more examples of this online, for example the StackOverflow question:
http://stackoverflow.com/questions/32514410/logging-with-retrofit-2

Hi!
Thanks for the feedback.
Yes I actually had the same thoughts whilst working through this example. It is possible, I have also included some basic logging in my example app on Github. You can find the code here:
https://github.com/spongebobrf/android-retrofit-test-examples/blob/master/RetrofitTestExample/app/src/main/java/za/co/riggaroo/retrofittestexample/interceptor/LoggingInterceptor.java
Obviously you might want to do a bit more logging.

There are more examples of this online, for example the StackOverflow question:
http://stackoverflow.com/questions/32514410/logging-with-retrofit-2

Wonderful the quote about the maintenance guy 😀 , and very interesting post ! Thank you a lot 🙂

Hi, this is a great writeup on Retrofit2. I am curious though, why your getQuoteOfTheDay() method calls showRetry() twice. Wouldn’t it be better to handle the error in one fell swoop, and call showRetry() once (with the right message)?

Hi Rebecca, do you know if retrofit2 support oauth2? or what is the best way to authenticate with oauth2?

hi Rebecca, thanks, and nice blog, i’m going to check out your link and I’ll tell you how it was

Good job Rebecca… I really liked this post. On the side note. Have you noticed that in recent beta version(beta4) they have changed the interfaces? Can you please update your post to include the updated beta version?

Great article…. after hunting for so many article found this cool post.
Thanks a lot 🙂

Thank you for the post. I have a question. When does the server.enqueue() get called? For example, I want to mock an api response after clicking a “Login” button which calls /api/login endpoint.

You need to call server.enqueue() – as you will be enqueuing the information yourself to the mock web server, so the information needs to be available on the mocked server before the API call gets executed.

Thanks for reply. I downloaded the source code and modified it to fit my needs.

Hi Rebecca,
I like the tutorial very much. However when I try applying this solution to my project I keep getting “Test running failed: Unable to find instrumentation info for: ComponentInfoTest running failed: Unable to find instrumentation info for: ComponentInfo{com.example.flavour1.demo.debug.test/android.support.test.runner.AndroidJUnitRunner}. I have the runner in defaultConfig section and have it selected in run configuration as well. Can you provide some pointers as to what to try next ? I’m really stuck reading almost 2 days with no success.

Thanks in advance,
Kalin.

Hi Kalin,
Have you included the android test runner dependency in your build.gradle file?
Make sure this is in your app level build.gradle:
androidTestCompile (“com.android.support.test:runner:0.4.1”)

Also, have you tried running the task using gradle by itself? So from command line (in your projects folder) try run ./gradlew connectedAndroidTest to see if it is maybe just an Android Studio configuration.

Do you know if it should also work with rx.Observable and rx.Subscriptions? I tried, but could not get it to work.

–Interface–
@POST(“auth/token”)
Observable authenticate(
@Query(“username”) String username,
@Query(“password”) String password,
@Query(“grant_type”) String grantType,
@Query(“scope”) String scope);

–Mock–
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(“{}”));

Hey! first of all, awesome post!
I was wondering how would I set my own custom error message for a failed response
You sent a Json on the following call, but I’m not sure how retrofit extracts the error from it?

Response errorResponse = Response.error(404, ResponseBody.create(MediaType.parse(“application/json”), “Error?”));

Nevermind, figured it out: It’s a bit convoluted but it works 🙂
Let me know if you know a btter way though

okhttp3.Response response = new okhttp3.Response.Builder()
.code(404)
.message(expected)
.protocol(Protocol.HTTP_1_1)
.request(new Request.Builder().url(“http://localhost/”).build())
.build();

Response<ArrayList> errorResponse = Response.error(ResponseBody.create(MediaType.parse(“application/json”), “”), response);

Leave a Reply