How to unit test Retrofit api calls?

54,558

Solution 1

I test my Retrofit callbacks using Mockito, Robolectric and Hamcrest libraries.

First of all, set up lib stack in your module's build.gradle:

dependencies {
    testCompile 'org.robolectric:robolectric:3.0'
    testCompile "org.mockito:mockito-core:1.10.19"
    androidTestCompile 'org.hamcrest:hamcrest-library:1.1'
}

In jour project's global build.gradle add following line to buildscript dependencies:

classpath 'org.robolectric:robolectric-gradle-plugin:1.0.1'

Then enter "Build Variants" menu in Android Studio (to quickly find it, hit Ctrl+Shift+A and search for it), and switch "Test Artifact" option to "Unit Tests". Android studio will switch your test folder to "com.your.package (test)" (instead of androidTest).

Ok. Set-up is done, time to write some tests!

Let's say you've got some retrofit api calls to retrieve a list of objects that need to be put into some adapter for a RecyclerView etc. We would like to test whether adapter gets filled with proper items on successful call. To do this, we'll need to switch your Retrofit interface implementation, that you use to make calls with a mock, and do some fake responses taking advantage of Mockito ArgumentCaptor class.

@Config(constants = BuildConfig.class, sdk = 21,
    manifest = "app/src/main/AndroidManifest.xml")
@RunWith(RobolectricGradleTestRunner.class)
public class RetrofitCallTest {

    private MainActivity mainActivity;

    @Mock
    private RetrofitApi mockRetrofitApiImpl;

    @Captor
    private ArgumentCaptor<Callback<List<YourObject>>> callbackArgumentCaptor;

    @Before
    public void setUp() {            
        MockitoAnnotations.initMocks(this);

        ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
        mainActivity = controller.get();

        // Then we need to swap the retrofit api impl. with a mock one
        // I usually store my Retrofit api impl as a static singleton in class RestClient, hence:
        RestClient.setApi(mockRetrofitApiImpl);

        controller.create();
    }

    @Test
    public void shouldFillAdapter() throws Exception {
        Mockito.verify(mockRetrofitApiImpl)
            .getYourObject(callbackArgumentCaptor.capture());

        int objectsQuantity = 10;
        List<YourObject> list = new ArrayList<YourObject>();
        for(int i = 0; i < objectsQuantity; ++i) {
            list.add(new YourObject());
        }

        callbackArgumentCaptor.getValue().success(list, null);

        YourAdapter yourAdapter = mainActivity.getAdapter(); // Obtain adapter
        // Simple test check if adapter has as many items as put into response
        assertThat(yourAdapter.getItemCount(), equalTo(objectsQuantity));
    }
}

Proceed with the test by right clicking the test class and hitting run.

And that's it. I strongly suggest using Robolectric (with robolectric gradle plugin) and Mockito, these libs make testing android apps whole lotta easier. I've learned this method from the following blog post. Also, refer to this answer.

Update: If you're using Retrofit with RxJava, check out my other answer on that too.

Solution 2

If you use .execute() instead of .enqueue() it makes execution synchron, thus the tests can ran properly without the need of importing 3 different libraries and adding any code or modify the build variants.

Like:

public class LoginAPITest {

    @Test
    public void login_Success() {

        APIEndpoints apiEndpoints = RetrofitHelper.getTesterInstance().create(APIEndpoints.class);

        Call<AuthResponse> call = apiEndpoints.postLogin();

        try {
            //Magic is here at .execute() instead of .enqueue()
            Response<AuthResponse> response = call.execute();
            AuthResponse authResponse = response.body();

            assertTrue(response.isSuccessful() && authResponse.getBearer().startsWith("TestBearer"));

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

Solution 3

  • The JUnit framework never executes the code in the CallBack functions because the main thread of execution terminates before the response is retrieved. You can use CountDownLatch as shown below:

    @Test
    public void testApiResponse() {
        CountDownLatch latch = new CountDownLatch(1);
        mApiHelper.loadDataFromBackend(new Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                System.out.println("Success");
                latch.countDown();
            }
    
            @Override
            public void onFailure(Call call, Throwable t) {
                System.out.println("Failure");
                latch.countDown();
            }
        });
    
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
    }
    
  • This test sample may be helpful too.

  • My advice isn't to perform testing for the API responses in the android app. There are many external tools for this.

Solution 4

Junit will not wait for async tasks to complete. You can use CountDownLatch (elegant solution which does NOT require an external library) to block the thread, until you receive response from server or timeout.

You can use CountDownLatch. The await methods block until the current count reaches zero due to invocations of the countDown() method, after which all waiting threads are released and any subsequent invocations of await return immediately.

//Step 1: Do your background job 
 latch.countDown(); //Step 2 : On completion ; notify the count down latch that your async task is done
 
 latch.await(); // Step 3: keep waiting

OR you can specify a timeout in your await call

  try {
      latch.await(2000, TimeUnit.MILLISECONDS);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

Sample Test Case

void testBackgroundJob() {

        Latch latch = new CountDownLatch(1);

        //Do your async job
        Service.doSomething(new Callback() {

            @Override
            public void onResponse(){
                ACTUAL_RESULT = SUCCESS;
                latch.countDown(); // notify the count down latch
                // assertEquals(..
            }

        });

        //Wait for api response async
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        assertEquals(expectedResult, ACTUAL_RESULT);

    }

Solution 5

As @Islam Salah said:

The JUnit framework never executes the code in the CallBack functions because the main thread of execution terminates before the response is retrieved.

You can use awaitility to solve the problem. Check out this answer on StackOverflow.

Share:
54,558

Related videos on Youtube

AabidMulani
Author by

AabidMulani

Transformation Engineer Extreme Programming Agile Development Pair Programming Test Driven Development

Updated on July 05, 2022

Comments

  • AabidMulani
    AabidMulani almost 2 years

    I am trying to integrate Unit test cases for every chunk of code possible. But I am facing issues while adding test cases for api calls that are made through retrofit.

    The JUnit compiler never executes the code in the CallBack functions.

    There is another option of making all the api calls Synchronous for testing purpose, but that's not possible for every case in my app.

    How can I sort this out? I have to add test cases in the api calls by any means.

    • B.shruti
      B.shruti almost 5 years
      Could you please share a sample of how you have written a test case for api calls in the first place?
  • neonDion
    neonDion over 8 years
    I read the same blog post and found the explanations to be helpful. One sticking point for me though was the example. I tried reading the example and then implementing tests in a similar fashion on my project. What I learned, which wasn't clear from the test code snippet alone, was that in the test described in the blog post, there is an Activity that makes the network call in the onCreate() method. This call needs to happen so that in the test the call to Mockito.verify() can be run. If you don't make the actual "network" call in your application code, the test code won't run as is.
  • Jemshit Iskenderov
    Jemshit Iskenderov over 7 years
    This is not answer for the question
  • maciekjanusz
    maciekjanusz over 7 years
    @JemshitIskenderov how come? care to point out the problem with it?
  • Jemshit Iskenderov
    Jemshit Iskenderov over 7 years
    1) Question is jUnit, you introduced Roboelectric, hamcrest 2) It is about async callback not being executed, you never answered why it could happen, what is the problem... 3) You just showed what you do in your project, so it is not answer for this question
  • maciekjanusz
    maciekjanusz over 7 years
    @JemshitIskenderov You must've missed the "Any suggestion for another approach will be also helpful." part of the question. That's what this answer is - a suggested working approach.
  • maciekjanusz
    maciekjanusz over 7 years
    While that's a valid approach, the question states There is another option of making all the api calls Synchronous for testing purpose, but thats not possible for every case in my app.
  • Adam Varhegyi
    Adam Varhegyi over 7 years
    mybad mybad mybad
  • Mr.G
    Mr.G almost 7 years
    @maciekjanusz ive got t a question here , in my retrofit calls i dont have callback as an argument such as FormUrlEncoded POST("/oauth/token") Call<Authorization> requestApiToken(@Header("Authorization") String token,@FieldMap Map<String,String> fields); so how i can use ArgumentCaptor ?
  • akshay bhange
    akshay bhange over 5 years
    where exactly do you check what you've received in onSuccess method is what you were expecting. I see here you've passed a value to onSuccess on your own & expecting it in Adapter.
  • Farruh Habibullaev
    Farruh Habibullaev about 5 years
    This solution gave me a very helpful clue. Thanks
  • Samuel Owino
    Samuel Owino over 4 years
    What is RetrofitHelper? Is it a custom class or a dependency class
  • Girish
    Girish about 4 years
    @AdamVarhegyi same as Samuel question.
  • Dr.jacky
    Dr.jacky about 4 years
    I'm using execute() but still get SocketTimeoutException
  • Daniel Alder
    Daniel Alder about 3 years
    Would also like to see RetrofitHelper
  • ansh sachdeva
    ansh sachdeva over 2 years
    hey i use this quite often but now am using suspend on my api interface class where the functions of Call<x> resides. this doesn't work for such classes . so got any idea how to use with coroutines?