1. 概述

在这个教程中,我们将学习如何使用Mockito框架模拟私有字段。Mockito是与JUnit一起广泛使用的Java模拟框架。它本身并不直接支持模拟私有字段,但我们可以通过不同的方法在Mockito中实现这一功能。

让我们来看看其中的一些方法。

2. 项目设置

我们首先创建将在示例中使用的类。我们将创建一个带有私有字段的类以及一个测试类来测试它。

2.1. 源类

首先,我们创建一个简单的类,其中包含一个私有字段:

public class MockService {
    private final Person person = new Person("John Doe");
    
    public String getName() {
        return person.getName();
    }
}

MockService 类有一个类型为 Person 的私有字段 person。它还有一个 getName() 方法,用于返回人员的名字。如您所见,person 字段没有setter方法,所以我们无法直接设置字段或更改字段的值。

接下来,我们将创建 Person 类:

public class Person {
    private final String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}

Person 类有一个私有字段 name,以及一个获取字段值的方法。

2.2. 测试类

接下来,我们将创建一个测试类来测试 MockService 类:

public class MockServiceUnitTest {
    private Person mockedPerson;

    @BeforeEach
    public void setUp(){
        mockedPerson = mock(Person.class);
    }
}

我们创建 Person 类的一个实例,并使用Mockito对其进行模拟。在接下来的章节中,我们将探讨如何使用这个模拟的实例替换 MockService 类的私有字段。

3. 使用Java反射API启用模拟

一种设置私有字段的方法是使用Java反射API。这是一个好方法,因为它不需要额外的依赖。我们可以首先使字段可访问,然后将字段的值设置为模拟的实例。

让我们看看如何实现这一点:

@Test
void givenNameChangedWithReflection_whenGetName_thenReturnName() throws Exception {
    Class<?> mockServiceClass = Class.forName("com.baeldung.mockprivate.MockService");
    MockService mockService = (MockService) mockServiceClass.getDeclaredConstructor().newInstance();
    Field field = mockServiceClass.getDeclaredField("person");
    field.setAccessible(true);
    field.set(mockService, mockedPerson);

    when(mockedPerson.getName()).thenReturn("Jane Doe");

    Assertions.assertEquals("Jane Doe", mockService.getName());
}

我们使用 Class.forName() 方法获取 MockService 类的类对象。然后,我们使用 getDeclaredConstructor() 方法创建 MockService 类的一个实例。

接下来,我们使用 getDeclaredField() 方法获取 MockService 类的 person 字段。我们使用 setAccessible() 方法使字段可访问,然后使用 set() 方法将字段的值设置为模拟的实例。

最后,我们可以模拟 Person 类的 getName() 方法,并测试 MockService 类的 getName() 方法以返回模拟的值。

4. 使用JUnit 5启用模拟

类似于Java反射API,JUnit 5 也提供了设置私有字段的实用方法。我们可以使用JUnit 5的 ReflectionUtils 类来设置私有字段:

@Test
void givenNameChangedWithReflectionUtils_whenGetName_thenReturnName() throws Exception {
    MockService mockService = new MockService();
    Field field = ReflectionUtils
      .findFields(MockService.class, f -> f.getName().equals("person"),
        ReflectionUtils.HierarchyTraversalMode.TOP_DOWN)
      .get(0);

    field.setAccessible(true);
    field.set(mockService, mockedPerson);

    when(mockedPerson.getName()).thenReturn("Jane Doe");

    Assertions.assertEquals("Jane Doe", mockService.getName());
}

这个方法的工作方式与前一个方法相同。主要的区别在于获取字段的方式:

  • 我们使用 ReflectionUtils.findFields() 方法获取字段。
  • 它接受 MockService 类的类对象和一个谓词来找到字段。这里我们使用的谓词是查找名为 "person" 的字段。
  • 此外,当我们有类的层次结构,并希望在层次结构中找到字段时,需要指定 HierarchyTraversalMode。在我们的例子中,因为我们只有一个类,所以可以使用任何 TOP_DOWNBOTTOM_UP 模式。

这给我们提供了字段,然后我们可以再次设置字段的值并进行测试。

5. 使用Spring Test启用模拟

如果我们在项目中使用Spring,Spring Test提供了一个名为 [ReflectionTestUtils](/spring-reflection-test-utils) 的实用工具类来设置私有字段。

5.1. 依赖

首先,我们需要在项目中添加 Spring Test 依赖:

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

或者,如果你使用的是Spring Boot,可以使用 Spring Boot Starter Test 依赖来完成相同的事情。

5.2. 测试用例

接下来,在测试中使用这个类来启用模拟:

@Test
void givenNameChangedWithReflectionTestUtils_whenGetName_thenReturnName() throws Exception {
    MockService mockService = new MockService();

    ReflectionTestUtils.setField(mockService, "person", mockedPerson);

    when(mockedPerson.getName()).thenReturn("Jane Doe");
    Assertions.assertEquals("Jane Doe", mockService.getName());
}

在这里,我们使用 ReflectionTestUtils.setField() 方法设置私有字段。内部地,这也使用了Java反射API来设置字段,但消除了冗余代码。

6. 总结

在这篇文章中,我们研究了使用Mockito模拟私有字段的不同方法。我们探讨了Java反射API、JUnit 5和Spring Test来模拟私有字段。

如往常一样,示例代码可以在GitHub上找到。