Android 使用Powermock的单元测试的 Rule 顺序
在使用 Powermock 进行安卓的单元测试的编写时,出现了一些已经被调用了初始化的业务代码在执行测试时没有被初始化的情况,在此记录一下解决问题的过程。
初始项目
首先建立一个类 InitData
,代表需要被初始化的业务单例代码:
1 | object InitData { |
然后建立一个测试,需要在测试开始前对这个单例进行初始化,然后验证传入的值:
1 |
|
测试优化
像上面的代码能正常运行通过,但是:
- 大部分的初始化操作都很类似,容易出现代码复制粘贴的情况;
- 一些初始化代码可能很复杂,不适宜在每份测试代码里都出现;
- 环境和测试业务可能分属业务方,初始化代码需要修改时不方便直接修改其它业务方代码。
因此,将这些环境初始化独立成通用逻辑是优化代码结构的一个选择。
提取初始化 Rule
Junit 提供了 TestRule 接口,可以控制测试代码执行前、执行后运行特定的片段,适宜这种初始化场景,那么我们将这个初始化抽成一个 Rule:
1 | class InitRule : TestRule { |
那么测试代码就可以相应变成:
1 |
|
使用 Powermock
在我们的项目中,经常用到了 Powermock 来对依赖项进行 mock 操作。大家都知道,Powermock 是使用定制的 Classloader 加载测试类,达到替换被依赖类的目的。那么在与 Rule 的配合使用中就出现了问题:
1 |
|
在运行这个测试时,将会失败:
Init to 10
java.lang.AssertionError:
Expected :10
Actual :0
问题分析
根据输出,明明已经调用了初始化了,为什么在测试方法内还会说 initData
是默认值0呢?首先根据断点大法,看看测试执行过程中发生了什么。
Rule 的执行顺序
在各处打上断点之后,会发现自定义的 InitRule
先被执行,再执行 PowerMockRule
。似乎 Rule 的执行顺序对测试结果有影响。先查看 JUnit 里的 Rule
文档 对于顺序的描述:
However, if there are multiple fields (or methods) they will be applied in an order that depends on your JVM’s implementation of the reflection API, which is undefined, in general.
JUnit 本身不保证 Rule 的执行顺序,那么先关注一下 PowerMockRule
内部进行了什么操作。
PowerMockRule 做了什么
先从表象上看,可以观察到一个现象: InitRule
执行时引用到了 InitData
类,那么其单例对象在类加载时就会被创建好,拥有一个内存地址,可以在 IDE 的 debug 模式中被看到;
继续运行,在测试方法体被执行时再次观察,会发现 InitData
的单例对象地址发生了改变,成为了一个新的对象,在没有其它调用初始化的地方时,其中存着的值当然就是默认值 0 了。
那么,在运行过程中,单例对象为什么会被替换呢?
从上文提到的 Powermock 的原理中其实能有一个猜测:因为 Powermock 使用了自己定制的 Classloader ,那么会不会是因为新的 Classloader 加载了单例类之后生成了新的单例对象,并且在测试执行时使用了新对象导致的?
观察断点命中时的调用栈信息,我们能发现一点端倪:
在 AbstractClassloaderExecutor#executeWithClassLoader 中, Powermock 将测试类深度复制(deepclone)了一次,并且使用一个新的 classloader 去加载,然后才再执行测试代码。这个新的 classloader 是由 PowerMockRule
中返回的 PowerMockStatement
初始化的,由 MockClassLoaderFactory
生成的定制版,用处不言而喻当然是为了实现各种 mock 功能了。
由一个新的 classloader 加载测试类之后,其依赖的 InitData
类需要由相同的 classloader 加载,才能被正确使用。在重新加载的过程中生成了新的单例对象,其中的变量都保持着未初始化的状态。
由此可以扩展开来想,对于 PowerMockRule
这种需要更换 classloader 的环境操作,可以认为执行前和执行后是完全不相干的两套运行环境,切换环境前运行的所有初始化行为都不会对切换后的环境产生影响。因此我们定制的 InitRule
需要在 PowerMockRule
之后执行。
但是上文也提到了,对于 field 级的 Rule ,执行顺序是不被保证的,所幸 JUnit 提供了 RuleChain
来控制执行顺序。
使用 RuleChain 再次优化
JUnit 提供了 RuleChain
的方式来让我们手动地控制各种 Rule
的执行顺序。假设我们为测试类声明了这样一套 Rule:
1 |
|
那么,测试执行时将会严格按照 Rule1 -> Rule2 -> Rule3 的顺序进入,即执行各个 Rule 中 base.evaluate()
之前的语句,然后执行 @Test
测试方法,再按照 Rule3 -> Rule2 -> Rule1 的顺序退出,即执行 base.evaluate()
之后的语句。
但是,RuleChain
所接受的 Rule 需要是 TestRule
的子类,而 PowerMockRule
所使用的是即将要被废弃的 MethodRule
,并不兼容,需要进行一个适配转换的操作。
使用 Adapter
除了等待 PowerMockRule
被正式升级为 TestRule
之外,可以使用其 issue 列表 中提供的一个适配器,将 MethodRule
转换为 TestRule
实现:
1 | class TestRuleAdapter(private val rule: MethodRule) : TestRule { |
然后我们就可以优雅地使用 RuleChain
管理我们的测试 Rule 了:
1 | class AdapterRuleTest { |
还有一个执行位置
除了使用 Rule
@Before
初始化之外,JUnit 还提供了 @BeforeClass
这么一个注解来执行一些操作,我们来试试能不能使用:
1 |
|
同样的,我们会得到一个断言错误:
java.lang.AssertionError:
Expected :10
Actual :0
可以得出结论, @BeforeClass
是在 Rule 之前被执行的。
结语
使用一个 Adapter 将 PowerMockRule 包装成 TestRule 之后,就能使用 RuleChain 完整地控制 Rule 执行顺序了,这样就可以简单地用上其他人包装好的 Rule ,简化自己的测试代码。
文中所写的示例代码已经上传到了我的 Github 仓库中,执行 test
将会在上文提到的错误使用的位置断言失败。
但是,这个 Adapter 只能使用在普通的测试中,参数化测试不能使用,需要后续再扩展。
本文标题:Android 使用Powermock的单元测试的 Rule 顺序
文章作者:Xiao
发布时间:2019-12-10
最后更新:2023-12-04