1. 概述

Mockito 3.4.0版本之前,是不支持直接mock静态方法,需要借助于PowerMockito 本文以最新 Mockito 版本讲解如何mock静态方法。

2. 准备工作

首先我们定义一个简单的静态工具类用于测试 - StaticUtils。它有两个静态方法,分别用来演示如何对无参、和有参静态方法的mock。

public class StaticUtils {

    private StaticUtils() {}

    public static List<Integer> range(int start, int end) {
        return IntStream.range(start, end)
          .boxed()
          .collect(Collectors.toList());
    }

    public static String name() {
        return "Baeldung";
    }
}

3. 测试静态方法的一些思考

有人可能会说,在编写整洁(Clean Code)的面向对象 代码时,我们不应该需要模拟静态类。这通常暗示了我们的应用存在设计问题。

为什么呢?首先,依赖于静态方法的类具有紧密的耦合性,其次,它几乎总是导致难以测试的代码。理想情况下,一个类不应当负责获取其依赖项,如果可能的话,它们应该由外部注入。

因此,始终值得检查我们是否可以重构代码以使其更具可测试性。 当然,这并不总是可能的,有时我们需要模拟静态方法。

4. Mock 无参静态方法

Mockito 3.4.0版本后,我们可以使用 Mockito.mockStatic(Class<T> classToMock) 来mock静态方法的调用。 其返回值是一个MockedStatic类型的模拟对象

注意返回的是一个 scoped mock object,它只在当前线程(thread-local)作用域内有效,用完需要close模拟对象,这就是为什么我们使用 try-with-resources,MockedStatic 继承了 AutoCloseable接口。

@Test
void givenStaticMethodWithNoArgs_whenMocked_thenReturnsMockSuccessfully() {
    assertThat(StaticUtils.name()).isEqualTo("Baeldung");

    try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
        // 模拟 StaticUtils.name()方法
        utilities.when(StaticUtils::name).thenReturn("Eugen");
        assertThat(StaticUtils.name()).isEqualTo("Eugen");
    }

    // 离开mock作用域后调用的是真实的方法
    assertThat(StaticUtils.name()).isEqualTo("Baeldung");
}

5. mock带有参数的静态方法

用法和无参静态方法类似,除了需要指定模拟的参数。

@Test
void givenStaticMethodWithArgs_whenMocked_thenReturnsMockSuccessfully() {
    assertThat(StaticUtils.range(2, 6)).containsExactly(2, 3, 4, 5);

    try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
        // mock `StaticUtils.range()` 方法,它有2个参数:
        utilities.when(() -> StaticUtils.range(2, 6))
          .thenReturn(Arrays.asList(10, 11, 12));

        assertThat(StaticUtils.range(2, 6)).containsExactly(10, 11, 12);
    }

    assertThat(StaticUtils.range(2, 6)).containsExactly(2, 3, 4, 5);
}

6. 解决MockitoException:Deregistering Existing Mock Registrations

在Java中,当试图在同一线程上下文中注册多个静态模拟时,通常会出现 “静态模拟已经在当前线程中注册” 的异常,违反了单次注册约束。 要解决这个问题,我们必须在创建新模拟之前先注销现有的静态模拟。

简单来说,我们需要:

  • 在每个线程中为静态模拟注册一次,最好使用如@Before这样的设置方法。
  • 在注册之前检查mock是否已经注册以防止冗余。
  • 在使用@After注册新的静态模拟之前,请先取消注册同一类的所有现有模拟。

以下是如何处理 “static mocking is already registered in the current thread” 异常的完整示例:

public class StaticMockRegistrationUnitTest {

    private MockedStatic<StaticUtils> mockStatic;

    @Before
    public void setUp() {
        // Registering a static mock for UserService before each test
        mockStatic = mockStatic(StaticUtils.class);
    }

    @After
    public void tearDown() {
        // Closing the mockStatic after each test
        mockStatic.close();
    }

    @Test
    public void givenStaticMockRegistration_whenMocked_thenReturnsMockSuccessfully() {
        // Ensure that the static mock for UserService is registered
        assertTrue(Mockito.mockingDetails(StaticUtils.class).isMock());
    }

    @Test
    public void givenAnotherStaticMockRegistration_whenMocked_thenReturnsMockSuccessfully() {
        // Ensure that the static mock for UserService is registered
        assertTrue(Mockito.mockingDetails(StaticUtils.class).isMock());
    }
}

在上述示例中,带有 @Before 注解的 setUp() 方法会在每个测试用例之前执行,确保一致的测试环境。在这个方法中,使用 mockStatic(StaticUtils.class)StaticUtils 注册静态模拟。这个注册过程确保每个测试前都会实例化一个新的静态模拟,保持测试的独立性,防止测试用例之间相互干扰。

相反,@After 注解的 tearDown() 方法会在每个测试用例后执行,释放测试执行期间获取的所有资源。

这个细致的设置和清理流程确保每个测试用例在其控制的环境中运行,促进可靠和可重现的测试结果,同时遵循单元测试的最佳实践。

7. 总结

在这篇简短的文章中,我们了解了一些如何使用Mockito模拟静态方法的例子。最后,文章的完整源代码可以在GitHub上找到。