I am writing a unit test for a simple method. The project I work in has the rule that a unit test must only test a single class (the class under test), any other classes that appear in the test must be mocked. The method under test accepts a single list; Mocking the input list results in some really weird behavior. The list seems to be empty and non-empty at the same time, even though I set the first element with Mockito.
class Sum {
static long sum(final List<Integer> list) {
if (list.isEmpty()) {
return -1L;
}
long sum = 0L;
for (int i = 0; i < list.size(); ++i) {
sum += list.get(i);
}
return sum;
}
}
class UnitTest {
@Test void sum_single() {
final List<Integer> listMock = Mockito.mock();
Mockito.when(listMock.get(0)).thenReturn(42);
assertFalse(listMock.isEmpty()); // list is non-empty
assertEquals(42, listMock.get(0)); // the element is in the list!
assertEquals(42, Sum.sum(listMock));
}
}
The test is failing with the following error:
org.opentest4j.AssertionFailedError: expected: <42> but was: <0>
What gives? Somehow, the element in the list is not summed, even though I have put it at index 0 in the list (I even verify the element in the test before testing my class). Furthermore, the list is obviously non-empty as verified in test itself (and if it were empty, the method would return -1).
Why is this test not working? It does everything according to the book: only a single class is tested, all other classes and collaborators are mocked, the mock is set up with a value to be used by the test. What do I have to change so that the test verifies that the elements in the list are correctly summed?
The problem is that Mockito.mock
creates a mock object with no stubbed behavior, which means that all methods will return a default value for their return type (empty collections, 0 for numbers, false for booleans, null for objects).
Stubbing the get
call does not magically add items to the list and does not affect the return value of isEmpty()
nor that of size()
. The mocked "list" is not a real list. It is a mock object without any behavior. An empty, lifeless shell. It only does what it is explicitly told in the test.
Applying the rules from the first paragraph lets us deduce:
mockList.isEmpty()
returns false
(the default value for a boolean)mockList.size()
returns 0
(the default value for a numeric type)If Mockito is applied carelessly, you'll end up with zombie objects, which pretend that they are working correctly, while they do not behave like their real counterparts. In case of the list, it seems to be non-empty but has a size of 0 at the same time; that doesn't make sense for a list! This also means your tests could break if the implementation is refactored to use list.isEmpty()
instead of the equivalent list.size() == 0
.
Not everything must be mocked. It is okay to test the interaction between objects (of the same unit). A unit is not a single class! (but it could be). Even Mockito itself advises against mocking everything:
If everything is mocked, are we really testing the production code? Don't hesitate to not mock!
On the same page, they explain that value objects shouldn't be mocked either. A list is clearly a value object (it is a data holder without any interesting "external" behavior).
Last but not least, don't mock types that you don't own.
The test becomes a lot shorter, simpler, and more correct by using a real List instance – and it actually works and is testing your logic:
class UnitTest {
@Test void sum_single() {
final List<Integer> list = List.of(42);
assertEquals(42, Sum.sum(list));
}
}
TLDR: Only mock if necessary. It is often easier to start without mocks and only introduce mock objects if it actually makes your tests simpler and more maintainable or faster to execute. Mocks are useful to hide external, complex behavior or to abstract IO, but they shouldn't be used unconditionally for everything. A "unit" can be more than a single class or a single method.
To quote the Mockito wiki one final time:
Don't hesitate to not mock!