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 verify
ing 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");
}