多线程环境中使用 Mockito 来 Mock 静态方法
回看三年前的一篇日志 Mockito 3.4.0 开始可 Mock 静态方法,最后对 Mockito 产生的缺憾是它无法用来 Mock 非测试线程(主线程)中的静态方法调用。其实这也是可以变通的,下面慢慢道来。
首先回顾一下 Mockito 的静态方法 Mock 的使用方法,随着 Mockito 版本的升级,引入依赖的方式也发生了些许的变化,以 Maven 项目为例,如果在 JUnit 5 下用 Mockito 的 pom.xml 依赖中为
由它引入的全部相关依赖
如今不再使用 mockito-inline 依赖了,以及它的 mockito-extensions/org.mockito.plugins.MockMaker(mock-make-inline) 和 mockito-extensions/org.mockito.plugins.MemberAccessor(memer-accessor-module)。
测试代码
Mock 了静态方法 LocalDate.of(year, month, day), 所以在 try(MockedStatic) 块中无论传入什么参数给 LocalDate.of() 方法返回的都是 1970-01-01
try(MockedStatic) 块只对它所在的线程起作用,如果 LocalDate.of() 是在其他线程中调用就无能为力了。
比如用下方的代码演示非主线程(测试线程)中调用 LocalDate.of()时不能被 Mock 的效果
这个测试就会失败了
为演示全过程,先创建一个待测试的类 MockitoDemo
如果不管不顾线程池的角色,写如下的测试代码
测试也将无法通过
测试通过
主要的行为就是覆盖了 execute(Runnable command) 方法,直接执行 command.run(), 所起的作用就是提交到 threadPool 的任务全部直接调用创建该 threadPool 实例的线程去执行,即实现了一个只有主线程(测试线程)的线程池,从而使得 try(MockedStatic) 与 MockitoDemo.getLocalDate() 中的 LocalDate.of(2024, 10, 10) 由同一个线程执行。
再次运行
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
首先回顾一下 Mockito 的静态方法 Mock 的使用方法,随着 Mockito 版本的升级,引入依赖的方式也发生了些许的变化,以 Maven 项目为例,如果在 JUnit 5 下用 Mockito 的 pom.xml 依赖中为
1<dependency>
2 <groupId>org.mockito</groupId>
3 <artifactId>mockito-junit-jupiter</artifactId>
4 <version>5.14.1</version>
5 <scope>test</scope>
6</dependency>由它引入的全部相关依赖
如今不再使用 mockito-inline 依赖了,以及它的 mockito-extensions/org.mockito.plugins.MockMaker(mock-make-inline) 和 mockito-extensions/org.mockito.plugins.MemberAccessor(memer-accessor-module)。测试代码
1public class MockitoDemoTest {
2
3 @Test
4 public void testMockitoMockStatic() {
5 LocalDate yearOf1970 = LocalDate.of(1970, 1, 1);
6 try (MockedStatic<LocalDate> theMock = Mockito.mockStatic(LocalDate.class)) {
7 theMock.when(() -> LocalDate.of(anyInt(), anyInt(), anyInt())).thenReturn(yearOf1970);
8
9 LocalDate localDate = LocalDate.of(2024, 10, 10);
10 assertEquals(1970, localDate.getYear()); // Ok
11 }
12 }
13}Mock 了静态方法 LocalDate.of(year, month, day), 所以在 try(MockedStatic) 块中无论传入什么参数给 LocalDate.of() 方法返回的都是 1970-01-01
try(MockedStatic) 块只对它所在的线程起作用,如果 LocalDate.of() 是在其他线程中调用就无能为力了。
比如用下方的代码演示非主线程(测试线程)中调用 LocalDate.of()时不能被 Mock 的效果
1public class MockitoDemoTest {
2
3 @Test
4 public void testMockitoMockStatic() {
5 LocalDate yearOf1970 = LocalDate.of(1970, 1, 1);
6 try (MockedStatic<LocalDate> theMock = Mockito.mockStatic(LocalDate.class)) {
7 theMock.when(() -> LocalDate.of(anyInt(), anyInt(), anyInt())).thenReturn(yearOf1970);
8
9 AtomicReference<LocalDate> localDateRef = new AtomicReference<>();
10 Thread thread = new Thread(() -> localDateRef.set(LocalDate.of(2024, 10, 10)));
11 thread.start();
12 thread.join();
13
14 assertEquals(1970, localDateRef.get().getYear()); // Failed
15 } catch (InterruptedException e) {
16 throw new RuntimeException(e);
17 }
18 }
19}这个测试就会失败了
MockitoDemoTest.testMockitoMockStatic:49 expected: <1970> but was: <2024>不过实际应用中我们多用线程池,不是说 try(MockedStatic) 只对它所在的线程起作用吗,那么我们可以对线程池来个腾龙换鸟,把主线程作为线程池中的唯一线程,这样所有的代码都会在 try(MockedStatic) 所在的线程(主线程)中执行了。
为演示全过程,先创建一个待测试的类 MockitoDemo
1public class MockitoDemo {
2
3 private ExecutorService threadPool;
4
5 public MockitoDemo(ExecutorService threadPool) {
6 this.threadPool = threadPool;
7 }
8
9 public LocalDate getLocalDate() {
10 Future<LocalDate> future = threadPool.submit(() -> LocalDate.of(2024, 10, 10));
11 try {
12 return future.get();
13 } catch (Exception e) {
14 throw new RuntimeException(e);
15 }
16 }
17}如果不管不顾线程池的角色,写如下的测试代码
1public class MockitoDemoTest {
2
3 @Test
4 public void testMockitoMockStatic() {
5 MockitoDemo mockitoDemo = new MockitoDemo(Executors.newFixedThreadPool(2));
6
7 LocalDate yearOf1970 = LocalDate.of(1970, 1, 1);
8 try (MockedStatic<LocalDate> theMock = Mockito.mockStatic(LocalDate.class)) {
9 theMock.when(() -> LocalDate.of(anyInt(), anyInt(), anyInt())).thenReturn(yearOf1970);
10 LocalDate localDate = mockitoDemo.getLocalDate();
11 assertEquals(1970, localDate.getYear()); // Failed
12 }
13 }
14}测试也将无法通过
MockitoDemoTest.testMockitoMockStatic:46 expected: <1970> but was: <2024>变通要来了,针对线程池下手,用当前线程作为唯一线程的线程池替代它就行,完整的实现是
1public class MockitoDemoTest {
2
3 @Test
4 public void testMockitoMockStatic() {
5 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()) {
6 @Override
7 public void execute(Runnable command) {
8 command.run();
9 }
10 };
11 MockitoDemo mockitoDemo = new MockitoDemo(threadPool);
12
13 LocalDate yearOf1970 = LocalDate.of(1970, 1, 1);
14 try (MockedStatic<LocalDate> theMock = Mockito.mockStatic(LocalDate.class)) {
15 theMock.when(() -> LocalDate.of(anyInt(), anyInt(), anyInt())).thenReturn(yearOf1970);
16 LocalDate localDate = mockitoDemo.getLocalDate();
17 assertEquals(1970, localDate.getYear()); // Ok
18 }
19 }
20}测试通过
主要的行为就是覆盖了 execute(Runnable command) 方法,直接执行 command.run(), 所起的作用就是提交到 threadPool 的任务全部直接调用创建该 threadPool 实例的线程去执行,即实现了一个只有主线程(测试线程)的线程池,从而使得 try(MockedStatic) 与 MockitoDemo.getLocalDate() 中的 LocalDate.of(2024, 10, 10) 由同一个线程执行。
话外
Maven 中使用当前的 Mockito 5.14.1 版本时,运行 mvn test 会看到如下控制台输出Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build what is described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3此篇测试用的 JDK 版本为 21, 参考信息所指引的链接 https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3,在 pom.xml 中加上
1 <plugin>
2 <groupId>org.apache.maven.plugins</groupId>
3 <artifactId>maven-dependency-plugin</artifactId>
4 <executions>
5 <execution>
6 <goals>
7 <goal>properties</goal>
8 </goals>
9 </execution>
10 </executions>
11 </plugin>
12 <plugin>
13 <groupId>org.apache.maven.plugins</groupId>
14 <artifactId>maven-surefire-plugin</artifactId>
15 <configuration>
16 <argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar}</argLine>
17 </configuration>
18 </plugin>再次运行
mvn test, 提示信息便消失了。
永久链接 https://yanbin.blog/mockito-mock-static-method-in-multiple-threading-env/, 来自 隔叶黄莺 Yanbin's Blog[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。