14

I separated retrofit api calls methods from the activity code and I want to do a unit test on these methods, one example: The interface:

public interface LoginService {
    @GET("/auth")
    public void basicLogin(Callback<AuthObject> response);
}

and this is the method that do the call, in the main activity I get the object by the event bus.

public class AuthAPI {
    private Bus bus;
    LoginService loginService;

    public AuthAPI(String username, String password) {
        this.bus = BusProvider.getInstance().getBus();
        loginService = ServiceGenerator.createService(LoginService.class,
                CommonUtils.BASE_URL,
                username,
                password);
    }

    public void Login() {

        loginService.basicLogin(new Callback<AuthObject>() {
            @Override
            public void success(AuthObject authObject, Response response) {
                bus.post(authObject);
            }

            @Override
            public void failure(RetrofitError error) {
                AuthObject authObject = new AuthObject();
                authObject.setError(true);
                bus.post(authObject);
            }
        });
    }

}

And here the test

@RunWith(MockitoJUnitRunner.class)
public class AuthCallTest extends TestCase {

    AuthAPI authAPI;

    @Mock
    private LoginService mockApi;

    @Captor
    private ArgumentCaptor<Callback<AuthObject>> cb;

    @Before
    public void setUp() throws Exception {
        authAPI = new AuthAPI("username", "password");
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testLogin() throws Exception {

        Mockito.verify(mockApi).basicLogin((cb.capture()));

        AuthObject authObject = new AuthObject();
        cb.getValue().success(authObject, null);

        assertEquals(authObject.isError(), false);
    }
}

when I launch the test I have this error

Wanted but not invoked:
mockApi.basicLogin(<Capturing argument>);
-> at AuthCallTest.testLogin(AuthCallTest.java:42)
Actually, there were zero interactions with this mock.

What I did wrong, this is driving me crazy I tried to follow this guide without success: http://www.mdswanson.com/blog/2013/12/16/reliable-android-http-testing-with-retrofit-and-mockito.html

someone help me :(

  • The problem is you are following a blog that is over two years old. Furthermore, that author didn't update his code for two years and has no interest to. – IgorGanapolsky Aug 26 '15 at 20:38

2 Answers2

21

The article isn't very clear as it misses out the setup steps. By visiting the GitHub project linked in the article, you can see the full source code which explains those missing steps:

1) The code samples are extracted from a test class testing a specific activity. As part of the setup (i.e. in @Before), it replaces the Activity's reference to a GitHub API implementation with a mock one. It then calls the Activity's onCreate().

2) During onCreate(), the activity makes a call to the now-replaced GitHub API, passing in its Callback object.

Those first two steps explain why the Mockito.verify(mockApi).repositories(Mockito.anyString(), cb.capture()); step at the beginning of each test works. As the test is run after @Before, the mockApi has indeed had a call on its repositories() method.

The rest of the code is easier to understand once that's in place. As he's only created a mockApi, but not changed the actual Callback being used, the activity's content is changed. The rest of the code then verifies that those changes have taken place, either by checking a ListView or the Toasts.


So to answer your question, you need to:

1) At the start of your test method, replace the AuthAPI's loginService object with your mockApi object, then call AuthAPI.Login().

2) Use verify() as you already are to check that the function has been called.

3) Create a sample AuthObject and pass it to the cb.getValue().success() function.

4) Obtain the AuthObject from your Bus and assert that it is the same one you sent to the callback.success() function.

This tests that your AuthAPI.Login() correctly sends to your Bus the AuthObject that it would retrieve from Retrofit.


(I realise the SO question was written some time ago, but as I came across the same article and had the same confusion very recently, I thought this answer could be useful for others.)

Steve Haley
  • 55,374
  • 17
  • 77
  • 85
  • It seems like you want to instrument your tests with Android activity context. I don't think this is the purest approach to testing REST APIs - which should be tested on the JVM (junit). – IgorGanapolsky Aug 26 '15 at 20:40
  • 1
    @IgorGanapolsky You can't directly test REST APIs in an Android app in JUnit if you use any class that references the Android SDK, as JUnit doesn't load the Android classes. However, that's the purpose of the article above - how to use Robolectric and Mockito to perform local tests of REST APIs without making actual server calls, while still accessing real Android classes. – Steve Haley Aug 27 '15 at 12:45
  • @SteveHaley The problem with the article above is that it is two years old. That is ancient in Android dev sense. That code is hardly usable at this point. – IgorGanapolsky Aug 27 '15 at 13:26
  • 1
    @IgorGanapolsky I viewed it more as an example/proof of concept than "this is the right way to do things". If you have a link to something newer, feel free to post it. – Steve Haley Aug 28 '15 at 12:39
  • @SteveHaley What good is a proof of concept if the code doesn't compile? Android Studio doesn't let you run proof of concepts. – IgorGanapolsky Aug 28 '15 at 14:06
  • 1
    @IgorGanapolsky Not being able to compile his code is an entirely different issue to whether Mockito paired with Robolectric is a good system to test Retrofit APIs. That's going back to his article being very unclear. With a bit of work to fill in the gaps, you can create working tests using the system he describes. As to whether Mockito+Robolectric is still the best way to do it, I couldn't say as I'm not familiar enough with the alternatives. – Steve Haley Aug 28 '15 at 14:26
1

The problem is that you call verify at the wrong moment: the purpose of verify is to verify that the interactions with mockApi were what you expected. So normally you would see something like:

authApi.login();
Mockito.verify(mockApi).basicLogin((cb.capture()));

That's also what the error message is telling you: verify expected basicLogin to be called but it wasn't.

I've read that article too and felt there was something missing. I don't actually undestand argument capture yet. So can't help you with that :)

user2143491
  • 341
  • 3
  • 8