如何正确测试依赖自动注入对象的 Spring Service 类

本文介绍在不启动 spring 容器的前提下,使用 mockito 对依赖 `@autowired` 外部服务的 spring service 进行单元测试的正确方法,重点解决因未手动注入 mock 导致的空指针异常问题。

在 Spring 应用中,@Autowired 字段注入仅在 Spring IoC 容器管理 Bean 的生命周期时生效(例如通过 @SpringBootTest 启动上下文)。而在纯单元测试(如使用 @ExtendWith(MockitoExtension.class))中,Spring 容器并未参与对象创建,因此 MyService 实例中的 externalService 字段保持为 null,直接调用会触发 NullPointerException。

正确的做法是:跳过 Spring 依赖注入机制,改用手动赋值方式将 Mock 对象注入目标 Service 实例。注意以下关键点:

  • ✅ 使用 @Mock 声明并初始化 ExternalService 的 Mock 实例;
  • ✅ 使用 new MyService() 构造原始实例(非 @Spy 或 @InjectMocks 的模糊代理);
  • ❌ 避免 Mockito.spy(new MyService()) 后未注入依赖——spy 仅包装原对象,不自动处理字段赋值;
  • ✅ 在测试方法内或 @BeforeEach 中,显式赋值 myService.externalService = externalService
  • ✅ 使用 Mockito.when(...).thenReturn(...) 定义 Mock 行为,推荐使用具体返回值(如 "mocked-response")而非 anyString()(后者在 thenReturn 中无效,应改为 thenReturn("test-result"))。

以下是修正后的完整示例代码:

@ExtendWith(MockitoExtension.class)
class MyServiceTest {

    @Mock
    private ExternalService externalService;

    private MyService myService; // 不要在此处初始化

    @BeforeEach
    void setUp() {
        myService = new MyService(); // 创建真实实例
        myService.externalService = externalService; // 手动注入 Mock
    }

    @Test
    void methodToTest_Test() {
        // 定义 Mock 行为:避免使用 anyString() 作为返回值
        when(externalService.call("input")).thenReturn("mocked-response");

        // 执行被测方法
        String result = myService.methodToTest("input");

        // 断言
        assertThat(result).is

EqualTo("mocked-response"); verify(externalService).call("input"); } }

⚠️ 注意事项:

  • @InjectMocks 虽可自动注入 @Mock 字段,但对 private 字段需配合 @Spy 或反射,稳定性不如手动赋值清晰可控;
  • 若 MyService 构造函数依赖 ExternalService,建议重构为构造器注入(推荐),此时可直接在 new MyService(externalService) 中完成依赖传递,更符合测试友好与 Spring 最佳实践;
  • 切勿在 thenReturn(anyString()) 中使用 anyString()——它仅用于匹配参数,不能作为返回值,否则将返回 null,引发后续 NPE。

总结:脱离 Spring 上下文的单元测试,本质是“普通 Java 对象测试”,所有依赖必须显式提供。手动注入 Mock 是最直接、可靠且易于理解的方式,兼顾可读性与可维护性。