以实际 Java 项目中的单元测试 Mock 框架基本是 Mockito 2 了,因为它有一个十分流畅的 API。Mockito 2也为 JUnit 5 配上了 MockitoExtension, 所以 JUnit 5 下使用 Mockito 2 的关节也打通了。但在我们享受 Mockito 2 便利的同时,与 JMockit 相比局限性就很明显,因为 Mockito 2 是通过创建匿名子类来进行 Mock 的,所以任何子类对父类无法突破的方面它都存在。譬如,final 类, final 方法, 私有方法, 静态方法, 构造函数都是无法通过子类型进行重写的。所以除非特别需要,在 Mockito 2 无法胜任时都求助于 JMockit,JMockit 借助于 javaagent 取得了 JVM 的高控制权才得已为所欲为。
当 Mockito 来到了 2.1.0 版本,它也觉得不能对以上所有的限制置若罔闻, 首先带给我们的突破是它也可以 Mock final 类和 final 方法,虽然仍处于孵化器中,但毕竟是应用在单元测试中,能用就很不错了,只要以后不被拿走就行。这是官方对它的介绍 Mock the unmockable: opt-in mocking of final classes/methods
下面我亲自操作一遍,并给出更全方位的测试样例
待测试类(final 类与 final 方法的组合)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class NonFinalClassWithFinalMethod { final String finalMethod() { return "something"; } } final class FinalClassWitnNonFinalMethod { String nonFinalMethod() { return "somthing"; } } final class FinalClassWithFinalMethod { final String finalMethod() { return "something"; } } |
对 final 修饰符视而不见的 Mock 测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package cc.unmi; import org.hamcrest.CoreMatchers; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class FinalClassTest { @Test public void testNonFinalClassWithFinalMethod() { NonFinalClassWithFinalMethod testMe = Mockito.mock(NonFinalClassWithFinalMethod.class); when(testMe.finalMethod()).thenReturn("hello"); assertThat(testMe.finalMethod(), CoreMatchers.equalTo("hello")); } @Test public void testFinalClassWithNonFinalMethod() { FinalClassWitnNonFinalMethod testMe = Mockito.mock(FinalClassWitnNonFinalMethod.class); when(testMe.nonFinalMethod()).thenReturn("hello"); assertThat(testMe.nonFinalMethod(), CoreMatchers.equalTo("hello")); } @Test public void testFinalClassWithFinalMethod() { FinalClassWithFinalMethod testMe = Mockito.mock(FinalClassWithFinalMethod.class); when(testMe.finalMethod()).thenReturn("hello"); assertThat(testMe.finalMethod(), CoreMatchers.equalTo("hello")); } } |
如果上面的待测试类没有 final 类和 final 方法,上面三个测试用例都能成功通过。然而基于待测试类存在 final 修饰符的事实,以上测试用例将全部失败,它们的错误信息将分别是
testNonFinalClassWithFinalMethod():
org.mockito.exceptions.misusing.MissingMethodInvocationException:
when() requires an argument which has to be 'a method call on a mock'.
For example:
when(mock.getArticles()).thenReturn(articles);
Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
Those methods *cannot* be stubbed/verified.
Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.
testFinalClassWithNonFinalMethod():
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class cc.unmi.FinalClassWitnNonFinalMethod
Mockito cannot mock/spy because :
- final class
testFinalClassWithFinalMethod:
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class cc.unmi.FinalClassWitnNonFinalMethod
Mockito cannot mock/spy because :
- final class
就是说正常情况下 final class, final method 等是不受 Mockito 待见的。
但是当再加上一个待测试类
1 2 3 4 5 |
class NonFinalClassWithNonFinalMethod { String nonFinalMethod() { return "something"; } } |
和测试用例
1 2 3 4 5 6 |
@Test public void testNonFinalClassWithNonFinalMethod() { NonFinalClassWithNonFinalMethod testMe = Mockito.mock(NonFinalClassWithNonFinalMethod.class); when(testMe.nonFinalMethod()).thenReturn("hello"); assertThat(testMe.nonFinalMethod(), CoreMatchers.equalTo("hello")); } |
四个测试用例一起运行结果又不一样了
此时单独运行 testNonFinalClassWithFinalMethod
还是提示与之前一样的错误
org.mockito.exceptions.misusing.MissingMethodInvocationException:
when() requires an argument which has to be 'a method call on a mock'.
For example:
when(mock.getArticles()).thenReturn(articles);
Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
Those methods *cannot* be stubbed/verified.
Mocking methods declared on non-public parent classes is not supported.
2. inside when() you don't call method on mock but on some other object.
添加配置文件达成对 final 类 final 方法的 Mock
那么 Mockito 声称能 Mock final 类和 final 方法的角决之道在哪里呢?一个额外的配置文件,对于 Maven 标准布局的项目,创建这个文件
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
如果是其他项目的话,应该只需保证在 classpath 下有文件 mockito-extensions/org.mockito.plugins.MockMaker
该文件的内容是
mock-maker-inline
对,它就像是 SPI 模式的类似 META-INF/services/org.junit.platform.engine.TestEngine
文件。
有了 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
文件,及内容 mock-maker-inline
后,再次运行上面的测试用例
全绿,说明 Mockito 能对 final 类和 final 方法进行 Mock 了。这已经是 Mockito 的一大进步,先别对 private, static 有所奢欲。
MockMaker 让 Mockito 发生了什么
我们可以通过以下几张图来了解在 src/test/resources/mockio-extensions/org.mockito.plugins.MockMaker
(mock-maker-inline
) 前后发生了什么
前前,考查 Mock 的对象
Mock 非 final 类创建子类
后后,再次考查 Mock 的对象
终于 final 类也可以 Mock 了, 不能生成子类,像是直接生成它的实例,也无可非
可是对于非 final 类型也不再生成子类了,和 mock final 类一样的行为了
基于 Mockito.mock(clazz)
直接创建实例这一事实,原理上我们可以使用 Mockito 来 Mock 构造函数的。
前前后后,针对接口和抽象类
对于接口和抽象类的 Mock 行为始终没变,总是必须创建它们的子类型。
为什么是 mock-maker-inline
既然前面猜测 src/resources/mockito-extensions/org.mockito.plugins.MockMaker
像是 SPI 模式,那么它大约就真的是了,的确是。其中可以写 mock-maker-inline
, 必定也可以写上别的什么,自然要查找下 MockMaker
接口有哪些实现
有三个实现可选,mock-maker-inline
毫无疑问对应的就是 InlineByteBuddyMockMaker
, 默认的实现方式也很容易想像得出来就是 SubclassByteBuddyMockMaker
, 猜测配置为 mock-maker-subclass
, 经验证不对。如果要显式的指定采用 SubclassByteBuddyMockMaker
, 那么 src/resource/mockto-extensions/org.mockito.plugins.MockMaker
文件的内容是
org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker
而要用 ByteBuddyMockMaker
的话,该文件内容是
org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker
Mockito 目前提供的所有三个 MockMaker 实现,只有 InlineByteBuddyMockMaker
能用来 Mock final 类和 final 方法,mock-maker-inline
也可以配置为完整的类名
org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker
细看 org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker
类能发现它其中使用到了 JDK 的 Instrumentation API 和 ByteBuddyAgent 在生成实例时对方法实现进行了替换。
PowerMockito(PowerMock) 是什么来头
根据 Mockito 预留下来的 MockMaker
的实现,实际上给了第三方实现进行扩展对静态或私有方法的 Mock 了,这也就是让 Mockito 让 PowerMock 有可乘之机。因此,如果要 Mock 静态或私有方法的话,除 JMockit 外还有另一选择,那就是 PowerMock,而且还能继续使用 Mockito 的 API。
有了一个基本的推测后,顺道看一下 PowerMock 带来了什么 MockMaker 实现,在 pom.xml
加入当前最新的 powermock-api-mockito
模块
1 2 3 4 5 6 |
<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>1.7.4</version> <scope>test</scope> </dependency> |
powermock-api-mockito2
在Maven 中央仓库正式版当前为 1.7.4, 所带的 Mockito 还是 2.8.9, 当前 Mockito 版本是 2.18.3, 落后也并不大。在 Maven 中央仓库还有 2.0.0-beta.5
的 powermock-api-mockito2
, 所带的 Mockito 2 是 2.10.0 版本。
powermock-api-mockito2
为 Mockito 2 提供了 MockMaker
的一个第三方实现是
org.powermock.api.mockito.mockmaker.PowerMockMaker
也正是它完善了 Mockito 2 对静态方法,私有方法,以及构造函数的 Mock 的,所以说本质上 powermock-api-mockito2
就是 Mockito 2 的一个扩展。
本文链接 https://yanbin.blog/mockito-mock-final-class-final-method/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
感谢,讲的很清楚
[…] 内容为 mock-maker-inline。之前写过一篇介绍:Mockto 也能 Mock final 类和 final 方法了,其中也探索了它的实现细节,使用到了 ByteBuddy […]
这句的mockito拼写错误了,mockito-extensions/org.mockio.plugins.MockMaker
已更正,谢谢
按照官方的方法创建了文件org.mockito.plugins.MockMaker 但是仍然出现。是我创建的方式不对吗
`Mockito cannot mock/spy because :
- final class
我使用的Mockito是2.8.9
用PowerMock又经常出现在mock final和 whennew final的时候失败经常返回的不是我mock的对象
可以在跑测试用例的时候测一下是否能从 classpath 加载到这个新建的文件。
谢谢。找到原因了,因为我同时使用了PowerMock所以添加上面那个文件不起作用。后来通过添加依赖解决的。
testImplementation "org.mockito:mockito-inline:2.8.9"