1. 简介

ReflectionTestUtils 是 Spring Test Context 框架的一部分,它提供了一组基于反射的工具方法,主要用于单元测试和集成测试场景中:

  • 设置非 public 字段的值
  • 调用非 public 方法
  • 注入依赖项(特别是用于绕过字段注入的限制)

本文将通过几个示例,带你掌握如何在单元测试中使用 ReflectionTestUtils

2. Maven 依赖

首先,我们需要添加必要的依赖项到 pom.xml 文件中:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.2.RELEASE</version>
    <scope>test</scope>
</dependency>

✅ 最新版本的 spring-contextspring-test 可以从 Maven Central 获取。

3. 使用 ReflectionTestUtils 设置私有字段的值

假设我们要测试一个类的实例,而这个类中有一个私有字段,并且没有提供 public 的 setter 方法。

我们先定义一个简单的 Employee 类:

public class Employee {
    private Integer id;
    private String name;

    // standard getters/setters
}

正常情况下,我们无法直接访问 id 字段来赋值,但可以借助 ReflectionTestUtils.setField 来搞定:

@Test
public void whenNonPublicField_thenReflectionTestUtilsSetField() {
    Employee employee = new Employee();
    ReflectionTestUtils.setField(employee, "id", 1);
 
    assertTrue(employee.getId().equals(1));
}

⚠️ 虽然能用,但这种方式属于“曲线救国”,尽量避免在生产代码中滥用。

4. 使用 ReflectionTestUtils 调用私有方法

假设我们在 Employee 类中有一个私有方法 employeeToString()

private String employeeToString(){
    return "id: " + getId() + "; name: " + getName();
}

虽然它是私有的,但我们依然可以通过 ReflectionTestUtils.invokeMethod 来调用它进行测试:

@Test
public void whenNonPublicMethod_thenReflectionTestUtilsInvokeMethod() {
    Employee employee = new Employee();
    ReflectionTestUtils.setField(employee, "id", 1);
    employee.setName("Smith, John");
 
    assertTrue(ReflectionTestUtils.invokeMethod(employee, "employeeToString")
      .equals("id: 1; name: Smith, John"));
}

✅ 这种方式在测试私有逻辑时非常有用,但记住,测试私有方法要慎重,优先考虑测试行为而非实现细节。

5. 使用 ReflectionTestUtils 注入依赖

假设我们有一个 Spring 组件 EmployeeService,其中依赖了一个通过 @Autowired 注入的私有字段:

@Component
public class EmployeeService {
 
    @Autowired
    private HRService hrService;

    public String findEmployeeStatus(Integer employeeId) {
        return "Employee " + employeeId + " status: " + hrService.getEmployeeStatus(employeeId);
    }
}

对应的 HRService 实现如下:

@Component
public class HRService {

    public String getEmployeeStatus(Integer employeeId) {
        return "Inactive";
    }
}

在测试中,我们想使用 Mockito 来 mock HRService,但由于它是私有字段,没有 setter,怎么办?

简单粗暴,直接上 ReflectionTestUtils.setField

HRService hrService = mock(HRService.class);
when(hrService.getEmployeeStatus(employee.getId())).thenReturn("Active");

注入 mock 实例:

EmployeeService employeeService = new EmployeeService();
ReflectionTestUtils.setField(employeeService, "hrService", hrService);

完整测试代码如下:

@Test
public void whenInjectingMockOfDependency_thenReflectionTestUtilsSetField() {
    Employee employee = new Employee();
    ReflectionTestUtils.setField(employee, "id", 1);
    employee.setName("Smith, John");

    HRService hrService = mock(HRService.class);
    when(hrService.getEmployeeStatus(employee.getId())).thenReturn("Active");
    EmployeeService employeeService = new EmployeeService();

    // Inject mock into the private field
    ReflectionTestUtils.setField(employeeService, "hrService", hrService);  

    assertEquals(
      "Employee " + employee.getId() + " status: Active", 
      employeeService.findEmployeeStatus(employee.getId()));
}

⚠️ 注意:这种做法只是因为使用了字段注入(field injection)才需要。如果你改用构造器注入(constructor injection),就完全不需要 ReflectionTestUtils 来注入依赖了。

6. 总结

在这篇文章中,我们通过几个典型示例展示了如何使用 ReflectionTestUtils 来处理以下常见测试难题:

  • 设置私有字段
  • 调用私有方法
  • 注入依赖项

虽然这些操作在测试中非常实用,但建议仅在必要时使用,特别是在涉及依赖注入时,优先使用构造器注入会更优雅。

📚 完整示例代码可以在 GitHub 上找到。


原始标题:Guide to ReflectionTestUtils for Unit Testing