Keywords: Mockito | Unit Testing | Java Testing
Abstract: This article provides an in-depth analysis of the common UnfinishedStubbingException in the Mockito framework, revealing the root cause through specific code examples. It explains Mockito's static state management mechanism, demonstrates how parameter evaluation order leads to stubbing interruptions, and offers best practices for code refactoring. The article also explores the trade-offs in Mockito's design philosophy, helping developers fundamentally understand and avoid such issues.
Problem Background and Exception Analysis
In unit test development, Mockito's UnfinishedStubbingException is a common pitfall. This exception typically occurs when developers attempt to nest new mocking calls within unfinished stubbing operations. Technically, this issue stems from Mockito's reliance on static state to track the current stubbing context.
Code Example and Problem Reproduction
Consider the following test code snippet:
@Test
public void myTest(){
MyMainModel mainModel = Mockito.mock(MyMainModel.class);
Mockito.when(mainModel.getList()).thenReturn(getSomeList()); // Line 355
}
private List<SomeModel> getSomeList() {
SomeModel model = Mockito.mock(SomeModel.class);
Mockito.when(model.getName()).thenReturn("SomeName"); // Line 276
Mockito.when(model.getAddress()).thenReturn("Address");
return Arrays.asList(model);
}
When executing this test, Mockito throws UnfinishedStubbingException at line 276. The key lies in Java's parameter evaluation mechanism: when calling thenReturn(getSomeList()), the JVM must first evaluate the getSomeList() parameter, which initiates new mocking operations before the mainModel.getList() stubbing is complete.
Mockito Working Mechanism Analysis
Mockito manages the stubbing process by maintaining an internal list of invocation records. When a method on a mock object is called, the framework records the call details. The when method reads the most recent call from this list and returns an OngoingStubbing object. Subsequently, calling the thenReturn method completes the stubbing configuration.
Normal stubbing flow:
// 1. Call mock method
mainModel.getList();
// 2. Initiate stubbing
Mockito.when(null);
// 3. Complete configuration
thenReturn(someModelList);
Problem flow analysis:
- Call
mainModel.getList()- Mockito records this call - Call
Mockito.when()- CreatesOngoingStubbingobject - Evaluate
getSomeList()parameter - New mocking operations begin - Call
model.getName()- Mockito confuses stubbing context
Solutions and Best Practices
Refactor test code to separate mocking operations:
@Test
public void myTest(){
MyMainModel mainModel = Mockito.mock(MyMainModel.class);
List<SomeModel> someModelList = getSomeList();
Mockito.when(mainModel.getList()).thenReturn(someModelList);
}
This refactoring ensures:
- All mocking operations complete before stubbing configuration
- Clear execution order avoids static state conflicts
- Improved code readability and maintainability
Design Philosophy Discussion
Mockito's choice of static state-based design, while potentially violating the "Principle of Least Astonishment" in some scenarios, provides test code with conciseness and expressiveness. Developers need to understand the logic behind this design decision to better leverage the framework's advantages.
Advanced Tips
Modern Mockito versions include more explicit hints in error messages: "you are stubbing the behaviour of another mock inside before 'thenReturn' instruction if completed." This directly addresses the core issue of nested mocking. Developers are advised to:
- Maintain atomicity in stubbing operations
- Avoid complex computations in stubbing parameters
- Use explicit variables to separate different levels of mocking