0

I am just getting up to speed with Mockito and I don't find it particularly useful.

I have a View and a Presenter. The view is a dumb activity and the presenter contains all the business logic. I want to mock the View and test the way the Presenter works.

Here comes Mockito, I can successfully mock the View and these two unit tests work just fine:

@Test
public void testWhenUserNameIsEmptyShowErrorOnLoginClicked() throws Exception {
    Mockito.when(loginView.getUserName()).thenReturn("");
    Mockito.when(loginView.getPassword()).thenReturn("asdasd");
    loginPresenter.setLoginView(loginView);
    loginPresenter.onLoginClicked();
    Mockito.verify(loginView).setEmailFieldErrorMessage();
}

@Test
public void testWhenPasswordIsEmptyShowErrorOnPasswordClicked() throws Exception {
    Mockito.when(loginView.getUserName()).thenReturn("George");
    Mockito.when(loginView.getPassword()).thenReturn("");
    loginPresenter.setLoginView(loginView);
    loginPresenter.onLoginClicked();
    Mockito.verify(loginView).setPasswordFieldErrorMessage();
}

However, if I want to test the presenter's inner methods, this doesn't work:

@Test
public void testWhenUserNameAndPasswordAreEnteredShouldAttemptLogin() throws Exception {
    LoginView loginView = Mockito.mock(LoginView.class);
    Mockito.when(loginView.getUserName()).thenReturn("George");
    Mockito.when(loginView.getPassword()).thenReturn("aaaaaa");
    loginPresenter.setLoginView(loginView);
    loginPresenter.onLoginClicked();
    Mockito.verify(loginPresenter).attemptLogin(loginView.getUserName(), loginView.getPassword());
}

it throws a NotAMockException - it says the object should be a Mock. Why would I want to test the mock? It is one of the first rules in testing - you don't create a mock and then test it, you have an object that you want to test and if it needs some dependencies - you mock them.

Maybe I don't understand Mockito properly but it seems useless to me this way. What do I do?

Kaloyan Roussev
  • 14,515
  • 21
  • 98
  • 180
  • This is exactly what I am saying, so why does Mockito want me to test the mock instead of the object under test? – Kaloyan Roussev Mar 21 '16 at 09:48
  • I mean I want to test the presenter and if one method in it invokes another method in it. I don't want to test the view, which is a mock. The presenter is the object under test, so I want to test it and I don't know why Mockito asks me to test the mock – Kaloyan Roussev Mar 21 '16 at 09:57
  • So this code is (unit) testing the view (in a file called XXXViewTest.java or something), are you putting tests for another unit in the same place? The error message is reasonable, it says that "loginPresenter" is a solid implementation and not a mock, because it isn't – BretC Mar 21 '16 at 14:02
  • The file is called LoginPresenterTest and I want to test the presenter (I took advantage of being able to assert method invocations in the view, in the first two tests, but basically I want to test the presenter) – Kaloyan Roussev Mar 21 '16 at 14:33

1 Answers1

1

Ideally, Mockito should only be used for mocking and verifying external services. You are correct to identify that the way you have it is sub-optimal, mostly because you're testing your implementation rather than your general contract.

// Doesn't work unless loginPresenter is a mock.
verify(loginPresenter)
    .attemptLogin(loginView.getUserName(), loginView.getPassword());

From a technical standpoint, Mockito can only stub and verify methods on mocks. This is because, under the surface, Mockito can't reach in and inspect every interaction between every class in your system; its "mocks" are subclasses or proxies that effectively override every single method to record interactions for verification and respond in the way that you've stubbed. This means that if you want to call when or verify as it applies to your presenter, it needs to be on a non-final non-static method on a mock or spy object, and you're right to observe that this would make it very easy to inadvertently test that Mockito works rather than testing that your unit or system works.

In your case, you seem to be treating your unit under test as the single onLoginClicked method, which includes stubbing and verifying its interactions with other methods on your presenter. This is called "partial mocking" and is actually a valid testing strategy for some cases, particularly when you're heavily testing a lightweight method and that lightweight method calls much heavier methods on the same object. Though you can generally avoid partial mocking with refactoring (and by designing testable components), it remains a useful tool in the toolbox.

// Partial mocking example
@Test
public void testWhenUserNameAndPasswordAreEnteredShouldAttemptLogin() {
  LoginView loginView = Mockito.mock(LoginView.class);

  // Use a spy, which delegates to the original object by default.
  loginPresenter = Mockito.spy(new LoginPresenter());

  Mockito.when(loginView.getUserName()).thenReturn("George");
  Mockito.when(loginView.getPassword()).thenReturn("aaaaaa");
  loginPresenter.setLoginView(loginView);
  loginPresenter.onLoginClicked();
  // Beware! You can get weird errors if calling a method on a mock in the
  // middle of stubbing or verification.
  Mockito.verify(loginPresenter)
      .attemptLogin(loginView.getUserName(), loginView.getPassword());
}

Of course, you can do the same without Mockito:

String capturedUsername;
String capturedPassword;

public void testWhenUserNameAndPasswordAreEnteredShouldAttemptLogin_noMockito() {
  // Same as above, with an anonymous inner class instead of Mockito.
  LoginView loginView = Mockito.mock(LoginView.class);
  loginPresenter = new LoginPresenter() {

    @Override public void attemptLogin(String username, String password) {
      capturedUsername = username;
      capturedPassword = password;
    }
  };

  Mockito.when(loginView.getUserName()).thenReturn("George");
  Mockito.when(loginView.getPassword()).thenReturn("aaaaaa");
  loginPresenter.setLoginView(loginView);
  loginPresenter.onLoginClicked();
  assertEquals("George", capturedUsername);
  assertEquals("aaaaaa", capturedPassword);
}

The way you've written it, though, a more valuable strategy may be to treat your entire Presenter as your unit under test and only test your presenter's external interactions. At this point, the call to attemptLogin should be an implementation detail that your test doesn't care about, which frees you to refactor it however you'd like.

What should happen when attemptLogin runs, externally? Maybe the external interaction here is that your Presenter kicks off an RPC to the path LoginEndpoint.Login with the right parameters. Then, rather than verifying the implementation detail within your presenter, you verify its interactions with the outside world—which is exactly what Mockito is designed to do.

@Test
public void testWhenUserNameAndPasswordAreEnteredShouldAttemptLogin() {
  LoginView loginView = Mockito.mock(LoginView.class);
  RpcService rpcService = Mockito.mock(RpcService.class);
  Mockito.when(loginView.getUserName()).thenReturn("George");
  Mockito.when(loginView.getPassword()).thenReturn("aaaaaa");
  loginPresenter.setLoginView(loginView);
  loginPresenter.setRpcService(rpcService);
  loginPresenter.onLoginClicked();
  Mockito.verify(rpcService).send("LoginEndpoint.Login", "George", "aaaaaa");
}
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • Perfect, that last but was the most useful. I should verify that LoginPresenter calls the next method in line, outside of it, on a mocked object. BTW I can't spy on LoginPresenter because it has been generated by Android Annotations and its a final class but thats another topic. Thanks! – Kaloyan Roussev Mar 22 '16 at 08:56
  • Can you help me out here as well? http://stackoverflow.com/questions/36150247/how-to-tell-the-subject-under-test-to-use-the-mock-instead-of-its-own-variable – Kaloyan Roussev Mar 22 '16 at 13:40
  • Glad to help! Sorry to say I'm out of my league with your follow-up, as I haven't used Android Annotations, but [this general question](http://stackoverflow.com/q/27552499/1426891) may lay out some options for you. – Jeff Bowman Mar 22 '16 at 17:54
  • Thanks, this is exactly what I needed – Kaloyan Roussev Mar 23 '16 at 08:34