Android单元测试在复杂项目里的落地姿势(PowerMock实践篇)

3,005 阅读4分钟

代码出处:colin-phang/AndroidUnitTest

上篇文章:Android单元测试在复杂项目里的落地姿势(调研篇)

上篇《调研》的结论是:

  1. Espresso需要跑在真机上,可用于依赖Android平台的功能测试。
  2. Roboelctric问题太多在复杂项目中寸步难行,弃了。
  3. 考虑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 {
...
}
  1. @RunWith,使测试代码运行于PowerMockRunner的环境下。
  2. @PrepareForTest,当需要Mock某个类的static、final、private方法的时候,就需要声明该注解。

上一篇提到,结合PowerMockito编写单元测试代码,遵循以下三个步骤:

  1. Mock被依赖的复杂对象
  2. 执行被测代码
  3. 验证逻辑是否按照预期执行/返回

而单元测试用例的编写,一部分取决于对业务代码的熟悉程度,另一方面则取决于对单元测试框架的了解程度,以下框架的很多用法具体还是需要自己去搜索资料并掌握的, 具体可以参考这两个文档:

  1. hehonghui/mockito-doc-zh
  2. 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 一些问题

  1. 单元测试覆盖率: 使用了PowerMock的@PrepareForTest修饰的类单元测试覆盖率变成0。这个问题暂时没看到解决方案。

5 参考文章

  1. 【腾讯TMQ】用Powermock和Mockito来做安卓单元测试
  2. 【美团技术团队】Android单元测试研究与实践
  3. Android 单元测试实战(1)—— 调研与选型