The problem stated in your question is a tricky one and the solution is "hacky" as well, so just to be clear (even though it's been stated in the question as well): I do not recommend using this approach, it would be best to refactor the code and avoid mocking construction of the ArrayList
. This answer is more of a "why we shouldn't go that way" showcase. That being said...
Mockito and the libraries it uses under the hood operate on the ArrayList
class, which in turn refer back to the Mockito's MockedConstruction
, which causes a StackOverflowError
in a scenario I verified. It is a problem also described in the Mockito GitHub Issues: here and here.
To work around that we may create a method like this:
static <T> MockedConstruction<T> mockCreationOnce(Class<T> clazz) {
// to operate on the MockedConstruction within a lambda passed to its creation
// we have to be able to pass the object to the lambda
// and to do that it needs to be final => AtomicReference usage
// (it could also be a custom class with a field set at a later time;
// the reference passed to the lambda has to be final, its contents may change)
var constructionHolder = new AtomicReference<MockedConstruction<T>>();
var listConstruction = Mockito.mockConstruction(
clazz,
context -> {
try {
return Mockito.withSettings();
} finally {
// we're closing the MockedConstruction after first call
// from within the doStuff() method
// to avoid the construction being mocked in Mockito internal calls
var construction = constructionHolder.get();
// we're using closeOnDemand() instead of close() to avoid exceptions here
// (no-op if closed already)
construction.closeOnDemand();
}
}
);
// here we're setting the MockedConstruction in the AtomicReference
// it happens before the lambda call,
// which will be invoked for the first (and last) time
// during ArrayList constructor call in the doStuff() method
constructionHolder.set(listConstruction);
return listConstruction;
}
Thanks to that we can verify the tested code (for example):
void doStuff() {
var numbers = new ArrayList<Integer>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
}
with such a test method:
@Test
void doStuff() {
var listConstruction = mockCreationOnce(ArrayList.class);
someService.doStuff();
var mockedList = listConstruction.constructed().get(0);
var inOrder = Mockito.inOrder(mockedList);
inOrder.verify(mockedList).add(1);
inOrder.verify(mockedList).add(2);
inOrder.verify(mockedList).add(3);
Mockito.verifyNoMoreInteractions(mockedList);
}
If we wanted to use an actual ArrayList
object as a spy here, it would not be possible, because of the problem described here - that's the source of the NullPointerException
. To work around that, explicit mocking can be used (which is another hack, showing that this is not the way to operate in the tests). For example for the tested code:
void doStuff() {
var numbers = new ArrayList<Integer>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
if (numbers.size() < 3) {
throw new AssertionError("Impossible state - size: " + numbers.size());
}
}
the test could look like this:
@Test
void doStuff() {
var listConstruction = mockCreationOnce(ArrayList.class);
// has to be done in a separate thread
// because the object needs to be created first in doStuff()
new Thread(() -> {
while (listConstruction.constructed().isEmpty()) {
// busy waiting for the construction to be finished before stubbing
}
var mock = listConstruction.constructed().get(0);
Mockito.doAnswer(inv -> 3)
.when(mock)
.size();
}).start();
someService.doStuff();
// no exception is thrown
}
Without the stubbing the test fails with AssertionError
. An important caveat regarding the snippet above: the stubbing mechanism needs polishing because now it's subject to a race condition and "random" test failure - some locking mechanism would have to be used to work around that. Still - in my opinion it would be simplest to work towards a proper solution instead of stacking workarounds in the tests.
I recreated the code above in a GitHub repository. I verified it - the test passes.