
本文详解junit+mockito单元测试中常见的nullpointerexception根源——手动new实例覆盖了@injectmocks自动注入的被测对象,导致依赖未初始化;通过修正对象创建方式、正确配置注解与初始化流程,可彻底避免此类问题。
本文详解junit+mockito单元测试中常见的nullpointerexception根源——手动new实例覆盖了@injectmocks自动注入的被测对象,导致依赖未初始化;通过修正对象创建方式、正确配置注解与初始化流程,可彻底避免此类问题。
在使用 Mockito 进行 Spring Boot 单元测试时,NullPointerException(NPE)是最常见却极易被忽视的问题之一。从你提供的代码和堆栈跟踪可以看出:异常发生在 RunSummaryServiceImpl.processCustomerRunSummary() 的第 74 行,即调用 runSummaryRepository.findByCustomerIdAndSolutionNumberAndDownloadDatetime(...) 时抛出——说明该 Repository 依赖为 null,根本原因并非 Mockito 配置错误,而是测试逻辑本身破坏了依赖注入机制。
? 根本原因:重复创建实例,绕过依赖注入
关键问题出现在测试方法内部:
@Test
public void TestprocessCustomerRunSummaries() {
final RunSummaryServiceImpl runSummaryServiceImpl = new RunSummaryServiceImpl(); // ❌ 错误!
// ...
runSummaryServiceImpl.processCustomerRunSummaries(runs); // 使用的是无依赖的裸对象 → NPE
}
尽管你在类级别声明并标注了:
@InjectMocks RunSummaryServiceImpl runSummaryServiceImpl; // ✅ 正确声明(但被下方 new 覆盖!)
但在测试方法中又执行了 new RunSummaryServiceImpl(),这会创建一个完全独立、未经 Mockito 注入任何 mock 依赖的原始实例。此时 runSummaryRepository 和 fileRepository 均为 null(Java 默认值),调用其方法必然触发 NPE。
✅ 正确做法:复用已注入的被测对象
只需删除 new 语句,直接使用类成员变量 runSummaryServiceImpl 即可:
@Test
public void testProcessCustomerRunSummaries() { // 方法名建议使用驼峰小写 + test 前缀
Collection<runsummary> runs = List.of(
new RunSummary("customer1", 1, Timestamp.valueOf("2023-05-30 10:00:00")),
new RunSummary("customer2", 2, Timestamp.valueOf("2023-05-30 11:00:00")),
new RunSummary("customer3", 3, Timestamp.valueOf("2023-05-30 12:00:00"))
);
// ✅ 直接调用已注入依赖的实例
runSummaryServiceImpl.processCustomerRunSummaries(runs);
}</runsummary>
⚠️ 注意:@InjectMocks 仅对当前测试类的成员变量生效,且要求该变量未被重新赋值。一旦执行 = new ...,就切断了 Mockito 的管理链。
?️ 补充最佳实践配置(适配现代 JUnit 5)
你当前使用 @Before + MockitoAnnotations.initMocks(this) 是 JUnit 4 风格,已过时。推荐升级为 JUnit 5 + MockitoExtension:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class) // 替代旧版 initMocks()
class RunSummaryServiceImplTest {
@Mock
private RunSummaryRepository runSummaryRepository;
@Mock
private FileRepository fileRepository;
@InjectMocks
private RunSummaryServiceImpl runSummaryServiceImpl; // 不再需要 @Spy 或 @SpringBootTest!
@Test
void testProcessCustomerRunSummaries() {
// 准备测试数据
Collection<runsummary> runs = List.of(
new RunSummary("custA", 1, Timestamp.valueOf("2023-05-30 10:00:00"))
);
// ✅ 直接调用,无需 new
runSummaryServiceImpl.processCustomerRunSummaries(runs);
// 可选:验证 repository 是否被调用(即使方法是 void)
verify(runSummaryRepository).findByCustomerIdAndSolutionNumberAndDownloadDatetime(
"custA", 1, Timestamp.valueOf("2023-05-30 10:00:00")
);
}
}</runsummary>
✅ 优势:
- 移除 @SpringBootTest:纯单元测试无需启动 Spring 上下文,更快更轻量;
- @Mock 替代 @Spy:除非需部分真实行为,否则 @Mock 更安全可控;
- @ExtendWith(MockitoExtension.class) 自动完成初始化,无需手动 initMocks;
- 方法名规范(testXXX),符合 JUnit 5 约定。
? 关键注意事项总结
- 永远不要在测试方法内 new 被 @InjectMocks 标注的类——这是引发 NPE 的头号陷阱;
- @InjectMocks 仅负责将 @Mock/@Spy 字段注入到目标对象中,不管理对象生命周期;
- 若服务类含 @Autowired 构造器注入(推荐),请改用构造器参数注入 mock,更清晰可靠;
- 对于 void 方法,可通过 verify(...) 断言其协作行为,而非返回值;
- 日志输出(如 LogManager.getLogger)在测试中通常无需 mock,除非涉及特定日志断言。
遵循以上原则,你的测试将稳定、快速、可维护,并真正发挥 Mockito 的价值——隔离依赖、聚焦逻辑验证。










