Mockito stubbing and verification with exact arguments are powerful testing techniques. Stubbing defines the expected behaviour of mocked objects, while verification checks how they are used. Both can help detect bugs early and make test code easier to understand.
However, Mockito also provides general argument matchers that, while convenient, can make tests less effective when the specific arguments of an interaction matter. Mockito's strict-stubbing features can help detect some unnecessary or mismatched stubs, but they are not a substitute for verifying that the correct interactions occur with the expected arguments.
Let's look at this simple function that transfers money from one account to the other:
public void transferMoney(String fromAccount, String toAccount, double amount) {
moneyService.debit(toAccount, amount);
moneyService.credit(fromAccount, amount);
}
A visual inspection reveals the obvious bug in the code: the implementation debits toAccount and credits fromAccount, so the accounts are reversed. But it was never detected because this was the unit test written for this function:
@Test
void shouldDebitAndCredit_whenTransferMoney_givenValidAccounts() {
transferMoney("IBAN1", "IBAN2", 10.00);
verify(moneyService).debit(anyString(), 10.00);
verify(moneyService).credit(anyString(), 10.00);
}
Had the test been written with exact argument verification, the bug would have been discovered straight away with this test failing:
@Test
void shouldDebitAndCredit_whenTransferMoney_givenValidAccounts() {
transferMoney("IBAN1", "IBAN2", 10.00);
verify(moneyService).debit("IBAN1", 10.00);
verify(moneyService).credit("IBAN2", 10.00);
}
This is especially important in test-driven development (TDD), where tests are often written before the implementation. Without precise argument verification, bugs like this can go undetected.
Let's look at another example:
public Optional<Manager> getByEmployee(String employeeId) {
return employeeRepository.findById(employeeId).map(Employee::getManager);
}
And here is the test that was written for it:
@Test
void shouldReturnManager_whenGetManager_givenEmployeeWithManager() {
when(employeeRepository.findById(anyString())).thenReturn(Optional.of(employee));
final var maybeManager = managerService.getByEmployee("UserWithId");
assertThat(maybeManager).contains(expectedManager);
}
At first glance, itβs hard to fault this test. But suppose the specification changes to convert all user IDs to lowercase before being passed to the database repository. A developer updates the implementation but accidentally converts the IDs to uppercase instead. The existing unit test fails to detect this mistake and the bug goes undetected. The correct unit test should therefore be:
@Test
void shouldReturnManager_whenGetManager_givenEmployeeWithManagerInCamelCase() {
when(employeeRepository.findById("userwithid")).thenReturn(Optional.of(employee));
final var maybeManager = managerService.getByEmployee("UserWithId");
assertThat(maybeManager).contains(expectedManager);
}
Of course, general argument matchers such as any() and anyString() are useful and sometimes even the best choice. However, when the specific arguments matter to the behaviour being tested, prefer exact arguments in your Mockito stubs and verifications. This makes tests more precise, expectations easier to understand, and bugs less likely to go undetected.
Top comments (0)