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

代码出处: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)—— 调研与选型
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容