代码出处:colin-phang/AndroidUnitTest
上篇文章:Android单元测试在复杂项目里的落地姿势(调研篇)
上篇《调研》的结论是:
- Espresso需要跑在真机上,可用于依赖Android平台的功能测试。
- Roboelctric问题太多在复杂项目中寸步难行,弃了。
- 考虑PowerMockito来隔离整个Android SDK以及项目业务的依赖,来保证单元测试代码能够快速有效地编写并执行。
文章主要分成 调研、 实践 两篇。 本篇主要讲讲基于PowerMockito如何在项目进行Android单元测试的实践。
1 依赖
参考:powermock/wiki
testImplementation 'junit:junit:4.12'
testImplementation "org.powermock:powermock-module-junit4:2.0.2"
testImplementation "org.powermock:powermock-api-mockito2:2.0.2"
按照上述引入PowerMock的依赖后即可在项目test目录下使用PowerMockito和Mockito了。
2 使用
0 基本使用
@RunWith(PowerMockRunner.class)
@PrepareForTest( { YourClassWithEgStaticMethod.class })
public class YourTestCase {
...
}
- @RunWith,使测试代码运行于PowerMockRunner的环境下。
- @PrepareForTest,当需要Mock某个类的static、final、private方法的时候,就需要声明该注解。
上一篇提到,结合PowerMockito编写单元测试代码,遵循以下三个步骤:
- Mock被依赖的复杂对象
- 执行被测代码
- 验证逻辑是否按照预期执行/返回
而单元测试用例的编写,一部分取决于对业务代码的熟悉程度,另一方面则取决于对单元测试框架的了解程度,以下框架的很多用法具体还是需要自己去搜索资料并掌握的,
具体可以参考这两个文档:
- hehonghui/mockito-doc-zh
- powermock/powermock
上篇文章也有一个简单的示例:PowerMockito在Android单元测试中的简单使用,这里不再赘述,下面说说在编写单元测试代码过程中,如何借助PowerMockito隔离Android SDK的依赖。
1 创建模拟对象的2种姿势
mock
activity = PowerMockito.mock(new MainActivity())
//使activity的isFinishing方法总是返回true
when(activity.isFinishing()).thenReturn(true);
通过mock创造出来的对象,调用该对象所有方法都不会执行真实逻辑。必须结合when(...).then(...)
来使模拟对象按照我们预期返回。
spy
activity = PowerMockito.spy(new MainActivity())
//使activity的isFinishing方法总是返回false
PowerMockito.doReturn(false).when(activity).isFinishing();
通过spy创造模拟对象必须先手动new出来,调用该对象所有方法都会执行真实逻辑。
spy对象必须结合doReturn(...).when(...)
才会忽略真实逻辑,并按照我们预期返回。
如果函数返回值为void,可以用
doNothing()
代替doReturn()
。
2 访问/调用private
参考 powermock/wiki/Bypass-Encapsulation
有时候被测类绝大部分是private函数(比如Activity),传统的单元测试很难覆盖到这些private函数,当然我们可以通过重构/封装使我们的业务代码对测试更友好,但为了测试而对原本稳定的业务代码进行侵入式的修改,在短期内肯定会带来不稳定因素,这往往是团队/领导无法容忍的。
PowerMock的Whitebox
类提供了一组api可以获取/修改private的变量和函数,可以帮助我们绕过重构去对业务代码进行测试。
//修改私有变量
Whitebox.setInternalState(..)
//访问私有变量
Whitebox.getInternalState(..)
//调用私有函数
Whitebox.invokeMethod(..)
//调用私有的构造函数
Whitebox.invokeConstructor(..)
非静态内部类的对象会隐式持有外部类对象,所以mock非静态内部类,需要给”this$0“的成员变量赋值,不然单元测试代码运行时会报错。
Whitebox.setInternalState(innerObj, "this$0", outerObj)
3 抑制不必要的代码逻辑执行
在实际项目中会有很多常用但不影响业务逻辑的代码(Log以及其他统计代码等等),有些静态代码块也直接调用Android SDK api。因为单元测试代码运行在JVM上,这些代码很容易会报错,如果为了测试去修改这些代码未免有点本末倒置,所以我们在单元测试的过程中需要抑制/隔离这些代码的执行。
抑制静态变量/代码块的执行
PowerMockito提供了@SuppressStaticInitializationFor
注解:
//在单元测试类之前声明以下注解,可以阻止FileUtil类的静态代码块运行
@SuppressStaticInitializationFor("com.colin.unittest.FileUtil")
public class PowerMockitoSampleIII {
...
}
抑制Log等静态函数的执行
借助mockStatic
可以使指定类的静态方法不执行。
@PrepareForTest(Log.class)
public class PowerMockitoSampleIII {
@Before
public void setUp() throws Exception {
//抑制Log相关代码的执行
PowerMockito.mockStatic(Log.class);
}
...
}
抑制super函数()的执行
实际业务开发中,我们经常需要继承Android SDK的类来进行扩展,对这些类覆写的函数进行单元测试时,往往需要抑制父类super()的逻辑,不然在JVM中执行单元测试代码时会报错。
//抑制MainActivity父类的onDestroy方法
Method method = PowerMockito.method(MainActivity.class.getSuperclass(),
"onDestroy");
PowerMockito.suppress(method);
3 结论
综上所述,在Android单元测试中,通过PowerMockito来隔离整个Android SDK以及项目业务的依赖,将单元测试的重心放在较细粒度(函数级别)的代码逻辑,完全可行。
4 一些问题
- 单元测试覆盖率:
使用了PowerMock的@PrepareForTest
修饰的类单元测试覆盖率变成0。这个问题暂时没看到解决方案。
5 参考文章
- 【腾讯TMQ】用Powermock和Mockito来做安卓单元测试
- 【美团技术团队】Android单元测试研究与实践
- Android 单元测试实战(1)—— 调研与选型