如何正确测试依赖@Autowired对象的Spring Service类

本文介绍在不启动spring容器的前提下,使用mo

ckito对依赖@autowired外部服务的spring service类进行单元测试的正确方法,重点解决因未手动注入mock对象导致的空指针异常问题。

在Spring应用中,@Autowired 依赖注入由Spring IoC容器在运行时自动完成;但在纯JUnit + Mockito的单元测试中(未启用@SpringBootTest),Spring容器并未启动,因此@Autowired字段不会被自动赋值——这正是externalService为null并引发NullPointerException的根本原因。

正确的做法是绕过Spring容器,手动完成依赖注入。以下是推荐的、符合测试最佳实践的完整方案:

✅ 推荐写法:使用构造函数注入 + Mockito.spy()(推荐)

首先,重构MyService以支持构造函数注入(更利于测试且符合Spring官方推荐):

@Service
public class MyService {
    private final ExternalService externalService;

    // 构造函数注入(推荐!)
    public MyService(ExternalService externalService) {
        this.externalService = externalService;
    }

    public String methodToTest(String myArg) {
        String response = externalService.call(myArg);
        // 处理逻辑...
        return "processed: " + response;
    }
}

对应测试类如下(简洁、安全、无需反射或字段赋值):

@ExtendWith(MockitoExtension.class)
class MyServiceTest {

    @Mock
    private ExternalService externalService;

    private MyService myService;

    @BeforeEach
    void setUp() {
        myService = new MyService(externalService); // 显式传入Mock
    }

    @Test
    void methodToTest_Test() {
        // 给Mock定义行为
        when(externalService.call("test")).thenReturn("mocked-response");

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

        // 验证结果与交互
        assertThat(result).isEqualTo("processed: mocked-response");
        verify(externalService).call("test");
    }
}

⚠️ 若无法修改原Service(如遗留代码):手动字段赋值(次选)

若必须保留@Autowired字段注入且不能改构造函数,则需在测试中通过反射或直接赋值注入Mock(注意:spy(new MyService())本身不会初始化externalService,必须显式设置):

@ExtendWith(MockitoExtension.class)
class MyServiceTest {

    @Mock
    private ExternalService externalService;

    private MyService myService;

    @BeforeEach
    void setUp() {
        myService = new MyService(); // 使用无参构造创建实例
        // 手动注入Mock(利用反射或直接赋值,此处假设字段为package-private或public)
        // 方式1:直接赋值(要求字段非private)
        // myService.externalService = externalService;

        // 方式2:反射注入(推荐用于private字段)
        ReflectionTestUtils.setField(myService, "externalService", externalService);
    }

    @Test
    void methodToTest_Test() {
        when(externalService.call("test")).thenReturn("mocked-response");
        String result = myService.methodToTest("test");
        assertThat(result).contains("mocked-response");
    }
}
? 关键提示: 避免使用 Mockito.spy(new MyService()) 后再尝试@Mock字段注入——spy()仅包装实例,不改变其内部字段状态; @InjectMocks 注解在字段注入场景下可能失效(尤其当存在多个构造函数或复杂依赖时),手动构造+注入更可控; 始终优先采用构造函数注入,它使依赖关系显式化、可测试性更强,也符合Spring Boot 2.6+ 的默认警告策略(spring.main.allow-circular-references=false等场景更稳健)。

通过以上方式,即可彻底规避NullPointerException,写出稳定、可维护、符合Spring工程规范的Service层单元测试。