Mockito 3.4.0 开始可 Mock 静态方法
Java 单元测试最趁手的 Mock 组件当属 Mockito,虽然它最初是基于继承来实现 Mock 的,所以对私有方法,私有属性,静态方法,
final 类,final 方法,构造函数无能为力。于是有时不得不引入 JMockit 或 PowerMockit 来辅助。不过现在的 Mockito 功力有所增强。
首先是 Mockito 2.1.0 开始可以 Mock final 类和 final 方法,要在 classpath 下创建个文件
从 Mockito 3.4.0 通过类似的 mockto-extensions 扩展的方式,实现了对静态方法的 Mock。所有使用到的接口是 org.mockito.MockedStatic, 它当前在 Mockito 3.7.7 中还是一个试验性方法 @Incubating,能拿来用就行。
换成
那么由
实质上
要是不想直接引入
测试 UtilsTest
测试通过,当前日期是 2021-02-03, 打印的日期是 2000-01-01, 又重回到了 2000 年。
这里我们用到了
theMock.when(LocalDate::now).thenReturn(yearOf2000)
而直接调用 LocalDate 的其他静态方法,如 LocalDate.of(2000, 1, 1) 则会报错,像下面的代码
运行后报错
而我们通常用的 Mockito.when(T methodCall) 的参数是一个方法调用的返回值,所以当 Mock 带参数的静态方法时与 Mockito.when(obj.foo(1, 2)).thenReturn(34)) 的用法是不一样的,MockedStatic.when() 的参数需要放 一个 () -> LocalDate.of(anyInt(), anyInt(), anyInt()) 这样的 Lambda. 完整例子如下
先改造一下 Utils 类,为
再就是下面的方式 Mock 带参数的静态方法
其实前面无参静态方法也可以把方法引用替代为一个 Lambda 表达式
并且注意到上面的第 14 行,对静态方法调用的 verify 也要用 theMock 的 verify() 方法,而不是 Mockito.verify()。
也是用的 ByteBuddy
只对当前线程对 LocalDate 静态方法的调用有效,看下面的例子就知道
输出为
链接:
永久链接 https://yanbin.blog/mockito-3-4-0-mock-static-method/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
首先是 Mockito 2.1.0 开始可以 Mock final 类和 final 方法,要在 classpath 下创建个文件
mockito-extensions/org.mockito.plugins.MockMaker, 内容为 mock-maker-inline。
之前写过一篇介绍:Mockto 也能 Mock final 类和 final 方法了,其中也探索了它的实现细节,使用到了 ByteBuddy 修改字节码。从 Mockito 3.4.0 通过类似的 mockto-extensions 扩展的方式,实现了对静态方法的 Mock。所有使用到的接口是 org.mockito.MockedStatic, 它当前在 Mockito 3.7.7 中还是一个试验性方法 @Incubating,能拿来用就行。
引入 mockito-inline 依赖
那么来看看 Mockito 是如何 Mock 静态方法,网上的文章一般是说要用mockito-inline 替换 mockito-core 依赖,也就是要在 pom.xml 中的依赖由通常的1<dependency>
2 <groupId>org.mockito</groupId>
3 <artifactId>mockito-core</artifactId>
4 <version>3.7.7</version>
5 <scope>test</scope>
6</dependency>换成
1<dependency>
2 <groupId>org.mockito</groupId>
3 <artifactId>mockito-inline</artifactId>
4 <version>3.7.7</version>
5 <scope>test</scope>
6</dependency>那么由
mockito-core 到 mockito-inline 发生了什么呢,看 mockito-inline 的
pom.xml
文件就知道,它其实内部依赖了 mockito-core。使用了 mockito-inline 后效果如下:
实质上 mockito-inline 就是给 mockito-core 添加了两个插件配置,分别是 org.mockito.plugins.MockMaker
和 org.mockito.plugins.MemberAccessor,而前者也是用于 Mock final 类和 final 方法的。
至于 Mock 静态方法是由前者还是后者支持的需要进一步研究。要是不想直接引入
mockito-inline 的话,自己在 classpath 下创建相同的文件及其内容也是可以的。测试 Mock 静态方法
有了mockito-inline 接着尝试下用代码如何 Mock 一个静态方法,先创建一个含有静态方法的待测试类 Utils 1package yanbin.blog;
2
3import java.time.LocalDate;
4
5public class Utils {
6
7 public LocalDate getCurrentDate() {
8 return LocalDate.now();
9 }
10}测试 UtilsTest
1package yanbin.blog;
2
3import org.junit.Test;
4import org.junit.runner.RunWith;
5import org.mockito.MockedStatic;
6import org.mockito.Mockito;
7import org.mockito.junit.MockitoJUnitRunner;
8
9import java.time.LocalDate;
10
11import static org.junit.Assert.assertEquals;
12
13@RunWith(MockitoJUnitRunner.class)
14public class UtilsTest {
15
16 private Utils target = new Utils();
17
18 @Test
19 public void testGetCurrentDate() {
20 LocalDate yearOf2000 = LocalDate.of(2000, 1, 1);
21 try (MockedStatic theMock = Mockito.mockStatic(LocalDate.class)) {
22 theMock.when(LocalDate::now).thenReturn(yearOf2000);
23
24 System.out.println(target.getCurrentDate());
25 assertEquals(2000, target.getCurrentDate().getYear());
26 }
27 }
28}测试通过,当前日期是 2021-02-03, 打印的日期是 2000-01-01, 又重回到了 2000 年。
这里我们用到了
Mockito.mockStatic(LocalDate.class) Mock 了 LocalDate 的所有的静态方法,在它 try 块外端调用 LocalDate 的静态方法是可以, 其间不用theMock.when(LocalDate::now).thenReturn(yearOf2000)
而直接调用 LocalDate 的其他静态方法,如 LocalDate.of(2000, 1, 1) 则会报错,像下面的代码
1@Test
2public void testGetCurrentDate() {
3 try (MockedStatic<LocalDate> theMock = Mockito.mockStatic(LocalDate.class)) {
4 theMock.when(LocalDate::now).thenReturn(LocalDate.of(2000, 1, 1));
5
6 System.out.println(target.getCurrentDate());
7 assertEquals(2000, target.getCurrentDate().getYear());
8 }
9}运行后报错
org.mockito.exceptions.misusing.UnfinishedStubbingException:也就是说用
Unfinished stubbing detected here:
-> at yanbin.blog.UtilsTest.testGetCurrentDate(UtilsTest.java:21) E.g. thenReturn() may be missing.
Examples of correct stubbing:
when(mock.isOk()).thenReturn(true);
when(mock.isOk()).thenThrow(exception);
doThrow(exception).when(mock).someVoidMethod();
Hints:
1. missing thenReturn()
2. you are trying to stub a final method, which is not supported
3. you are stubbing the behaviour of another mock inside before 'thenReturn' instruction is completed
at java.time.LocalDate.of(LocalDate.java:266)
at yanbin.blog.UtilsTest.testGetCurrentDate(UtilsTest.java:21)
............
try (MockedStatic theMock = Mockito.mockStatic(LocalDate.class)) { ... } 一旦 Mock 一个类,那么它所有的静态方法都被 Mockito 接管。Mock 带参数的静态方法
前面 Mock 是不带参数的静态方法,注意到MockedStatic.when(Verification) 的参数是一个 Verification,它是一个 SAM 接口1interface Verification {
2 void apply() throws Throwable;
3}而我们通常用的 Mockito.when(T methodCall) 的参数是一个方法调用的返回值,所以当 Mock 带参数的静态方法时与 Mockito.when(obj.foo(1, 2)).thenReturn(34)) 的用法是不一样的,MockedStatic.when() 的参数需要放 一个 () -> LocalDate.of(anyInt(), anyInt(), anyInt()) 这样的 Lambda. 完整例子如下
先改造一下 Utils 类,为
1package yanbin.blog;
2
3import java.time.LocalDate;
4
5public class Utils {
6
7 public LocalDate getDate(int year, int month, int day) {
8 return LocalDate.of(year, month, day);
9 }
10}再就是下面的方式 Mock 带参数的静态方法
1@RunWith(MockitoJUnitRunner.class)
2public class UtilsTest {
3
4 private Utils target = new Utils();
5
6 @Test
7 public void testGetCurrentDate() {
8 LocalDate yearOf1970 = LocalDate.of(1970, 1, 1);
9 try (MockedStatic<LocalDate> theMock = Mockito.mockStatic(LocalDate.class)) {
10 theMock.when(() -> LocalDate.of(anyInt(), anyInt(), anyInt())).thenReturn(yearOf1970);
11
12 System.out.println(target.getDate(2000, 2, 28));
13 assertEquals(1970, target.getDate(2000, 2, 28).getYear());
14 theMock.verify(()->LocalDate.of(anyInt(), anyInt(), anyInt()), times(1));
15 }
16 }
17}其实前面无参静态方法也可以把方法引用替代为一个 Lambda 表达式
theMock.when(() -> LocalDate.now()).thenReturn(yearOf1970);
并且注意到上面的第 14 行,对静态方法调用的 verify 也要用 theMock 的 verify() 方法,而不是 Mockito.verify()。
一张图大概看一下它的内部实现
也是用的 ByteBuddy再测试一下是哪个 Mockito 的插件在起作用
我们把依赖从mockito-inline 恢复到 mockito-core, 然后在 classpath
下自己创建 mockito-extensions/org.mockito.plugins.MockMaker 并内容mock-maker-inline重新运行最前面的 UtilsTest, 测试通过,说明
mock-maker-inline 同时支撑了对 final 类,final 方法和静态方法的 Mock。
那就是说明 mockito-extensions/org.mockito.plugins.MemberAccessor 的 member-accessor-module
增强了 Mockito 的其他特性,初步猜测是可用来替换测试类的内部成员属性值,新任务又来了。Mockito Mock 静态方法的弊端
在实际应用 Mockito 对静态方法进行 Mock 的时候,发现在多线程的时候失效,问题在于当用1try (MockedStatic<LocalDate> theMock = Mockito.mockStatic(LocalDate.class)) {
2 // 只对当前线程有效
3}只对当前线程对 LocalDate 静态方法的调用有效,看下面的例子就知道
1@Test
2public void testGetCurrentDate() throws InterruptedException {
3 LocalDate yearOf2000 = LocalDate.of(1970, 1, 1);
4 try (MockedStatic<LocalDate> theMock = Mockito.mockStatic(LocalDate.class)) {
5 theMock.when(LocalDate::now).thenReturn(yearOf2000);
6
7 System.out.println(Thread.currentThread().getName() + ":" + LocalDate.now()); //main:1970-01-01
8
9 Thread thread = new Thread(() ->
10 System.out.println(Thread.currentThread().getName() + ":" + LocalDate.now()) //Thread-0:2021-06-18,对于非当前线程无效
11 );
12 thread.start();
13 thread.join();
14
15 System.out.println(Thread.currentThread().getName() + ":" + LocalDate.now()); //main:1970-01-01
16 }
17 System.out.println(Thread.currentThread().getName() + ":" + LocalDate.now()); //main:2021-06-18, 退出 try block 后当然也失效
18}输出为
main:1970-01-01因为这一限制,几乎让 Mockito mock 静态方法这一表现淡然无色,在实际应用代码中线程的使用是非常普遍的。看来偶然间发现的亮点还得放弃掉。
Thread-0:2021-06-18
main:1970-01-01
main:2021-06-18
链接:
永久链接 https://yanbin.blog/mockito-3-4-0-mock-static-method/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。