Keywords: Mockito | NullPointerException | Unit Testing | Method Stubbing | Java Testing
Abstract: This article provides an in-depth analysis of the common causes of NullPointerException when stubbing methods in the Mockito testing framework, focusing on the cascading call issues caused by unstubbed methods returning null. Through detailed code examples, it introduces two core solutions: the complete stubbing chain approach and RETURNS_DEEP_STUBS configuration, supplemented by practical tips such as @RunWith annotation configuration and parameter matcher usage. The article also discusses best practices for test code to help developers avoid common Mockito pitfalls.
Problem Background and Phenomenon Analysis
When using Mockito for unit testing, developers often encounter NullPointerException while invoking the when().thenReturn() method. This typically occurs when attempting to stub a method of a mock object that hasn't been configured. Essentially, this is caused by Mockito's default behavior mechanism—for methods not explicitly stubbed, Mockito returns default values: false for boolean methods, empty collections for collection methods, and null for other methods.
Root Cause Analysis
Consider this typical scenario: when a developer writes when(myService.getListWithData(inputData).get()), if the myService.getListWithData(inputData) method hasn't been stubbed, Mockito returns null. Subsequently calling the .get() method throws a NullPointerException because it's invoking a method on a null reference.
The core of this issue lies in Mockito's stubbing mechanism being layer-by-layer. Each method call requires separate configuration; one cannot assume that intermediate call returns will be automatically created. While this design increases configuration clarity, it demands that developers have a complete understanding of the call chain.
Solution 1: Complete Stubbing Chain Approach
The most straightforward method is to explicitly stub each intermediate method in the call chain. Here's a complete example:
// Create intermediate mock objects
ListWithData listWithData = mock(ListWithData.class);
OutputData outputData = mock(OutputData.class);
// Configure stubs layer by layer
when(listWithData.get()).thenReturn(outputData);
when(outputData.getItemDatas()).thenReturn(allData);
when(myService.getListWithData(inputData)).thenReturn(listWithData);
Although this approach involves more code, it offers the highest readability and controllability. Each method's return value is explicitly specified, facilitating subsequent maintenance and understanding. It's particularly suitable for precise control in complex object relationships.
Solution 2: RETURNS_DEEP_STUBS Configuration
For deeply nested object call chains, Mockito provides the RETURNS_DEEP_STUBS configuration option, which automatically creates intermediate mock objects:
// Create a mock supporting deep stubs
SomeService myService = mock(SomeService.class, Mockito.RETURNS_DEEP_STUBS);
// Directly configure the final method
when(myService.getListWithData(inputData).get().getItemDatas()).thenReturn(allData);
This method significantly simplifies code but requires attention to its limitations: deep stubbing might conceal design issues, making overly high coupling between objects difficult to detect. It's recommended for simple scenarios, while complex business logic still favors the complete stubbing chain approach.
Additional Solutions and Best Practices
Beyond the two main solutions, other noteworthy configurations and techniques include:
Proper Test Runner Configuration
Ensure the test class uses correct annotation configuration:
@RunWith(MockitoJUnitRunner.class)
public class Test {
// Test code
}
This ensures Mockito annotations (like @Mock) are properly initialized, avoiding exceptions due to configuration issues.
Correct Usage of Parameter Matchers
When using parameter matchers, ensure type matching:
// Incorrect example - using any() for int parameter
when(myMock.thisFuncTakesAnInt(any())).thenReturn(someValue);
// Correct example - using type-specific matchers
when(myMock.thisFuncTakesAnInt(anyInt())).thenReturn(someValue);
Avoid Calling Setter Methods on Mock Objects
In test code, calling setter methods on mock objects is ineffective because mock objects don't actually execute these methods. Define mock object behavior through stubbing instead.
Complete Test Example
Here's a corrected complete test example demonstrating proper Mockito usage:
@RunWith(MockitoJUnitRunner.class)
public class ClassUnderTestTest {
@Mock
private SomeService myService;
@Mock
private OutputData outputData;
@InjectMocks
private ClassUnderTest classUnderTest;
private InputData inputData;
private List<ItemData> allData;
@Before
public void setUp() {
inputData = new InputData();
allData = Arrays.asList(mock(ItemData.class), mock(ItemData.class));
// Complete call chain configuration
when(outputData.getItemDatas()).thenReturn(allData);
when(myService.getListWithData(inputData)).thenReturn(Optional.of(outputData));
}
@Test
public void testGetInformations() {
Optional<OutputData> result = classUnderTest.getInformations(inputData);
assertTrue(result.isPresent());
assertEquals(allData, result.get().getItemDatas());
}
}
Summary and Recommendations
NullPointerException issues in Mockito often stem from insufficient understanding of the framework's mechanisms. By adopting complete stubbing chain configurations or appropriately using RETURNS_DEEP_STUBS, these problems can be effectively resolved. Additionally, good test code organization, correct annotation configuration, and proper assertion verification are key elements of writing high-quality unit tests.
In practical development, it's advised to: prioritize the complete stubbing chain approach for code clarity; consider deep stubbing only in simple scenarios; always verify test assertion conditions; and regularly review test code design quality. These practices will help developers build more reliable and maintainable test suites.