Android项目中的单元测试

Android项目中的单元测试

测试用例采用MVP + dagger架构,网络层采用Retrofit2 + Rxjava,使用登录做了一个简单测试。
测试框架使用:Junit、mockitorobolectric

一、首先看下针对MVP各层单元测试选型

在demo中,MVP各层所使用的单元测试框架如下图所示:


image
  • P层:不需要任何Android环境,因此使用Junit测试即可
  • V层:使用Robolectric进行UI的测试
  • M层:涉及到数据库、网络等相关操作,因此需要依赖Android环境,使用AndroidJUnitRunner进行测试

注:针对网络测试:需要将请求网络的异步请求转化成同步请求,才能获取到结果

二、测试框架引入说明

在无特殊需求的情况下可直接按下方式引入:

testImplementation 'junit:junit:4.12'//Junit测试框架,创建项目自动引入
//Mockito框架引入
testImplementation 'org.mockito:mockito-core:2.11.0'
//robolectric 开源测试ui库
testImplementation 'org.robolectric:robolectric:3.8'

其中mockito不支持mock匿名类、final类、static方法、private方法,如需支持可以使用PowerMock解决这些问题,如使用PowerMock则需引入以下库:

//引入PowerMock框架,必须对应mockito的版本,这里2.8.0-2.8.9 对应powermock的1.7.x版本
testImplementation 'org.mockito:mockito-core:2.8.9'
testImplementation 'org.powermock:powermock-module-junit4:1.7.3'
testImplementation 'org.powermock:powermock-api-mockito2:1.7.3'

如果引入的PowerMock使用@Rule规则进行mock则还需要引入另外两个库,这种方式可以有效的防止@RunWith被占用:

//注意这里是mockito2
//这里为使用@Rule方式引入要依赖的两个库,@Rule方式引入防止@RunWith被占用
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.3'
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.3'

注:引入均使用testImplementation方式,所以测试代码要写在test目录下,而非androidTest目录

三、几个测试框架的用法简单描述

1.Junit框架

Junit相信很多人都熟悉,测试Java代码基础测试工具。在Android创建项目的时候,默认添加的依赖就有Junit4.x版本的,主要功能就是断言。

常用的断言方法如下:
方法名 方法描述
assertEquals 断言传入的预期值与实际值是相等的
assertNotEquals 断言传入的预期值与实际值是不相等的
assertArrayEquals 断言传入的预期数组与实际数组是相等的
assertNull 断言传入的对象是不为空
assertNotNull 断言传入的对象是不为空
assertTrue 断言条件为真
assertFalse 断言条件为假
assertSame 断言两个对象引用同一个对象,相当于“==”
assertNotSame 断言两个对象引用不同的对象,相当于“!=”
assertThat 断言实际值是否满足指定的条件

注意:上面的每一个方法,都有对应的重载方法,可以在前面加一个String类型的参数,表示如果断言失败时的提示。

常用注解方法:
注解名 含义
@Test 表示此方法为测试方法
@Before 在每个测试方法前执行,可做初始化操作
@Afte 在每个测试方法后执行,可做释放资源操作
@Ignore 忽略的测试方法
@BeforeClass 在类中所有方法前运行。此注解修饰的方法必须是static void
@AfterClass 在类中最后运行。此注解修饰的方法必须是static void
@RunWith 指定该测试类使用某个运行器
@Parameters 指定测试类的测试数据集合
@Rule 重新制定测试类中方法的行为
@FixMethodOrder 指定测试类中方法的执行顺序

执行顺序:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass

简单例子:

public class ExampleUnitTest {

    @Rule
    public MyRule myRule = new MyRule();

    @Test
    public void addition_isCorrect() throws Exception {
        assertEquals(4, 2 + 2);
    }
}

2.Mockito框架使用简单说明

在实际的单元测试中,我们测试的类之间会有或多或少的耦合,导致我们无法顺利的进行测试,这时我们就可以使用Mock模拟一个对象,替换我们原先依赖的真实对象,这样我们就可以避免外部的影响,只测试本类,得到更准确的结果。当然它的功能不仅仅只是这些,测试驱动开发也是一大亮点。

Mockito是一个简单的Mock框架,可以帮助我们创建Mock对象,保持单元测试的独立性。

注:Mock出的对象全都是虚拟的对象,其中非void方法都将返回默认值,比如int方法将返回0,对象方法将返回null等,而void方法将什么都不做。比如:Mock一个List对象,如果获取这个List对象的size大小将返回是0

1.mockito有四种Mock方式,推荐使用下面方式:
@Mock       //<--使用@Mock注解
public User user;
@Rule       //<--使用@Rule
MockitoRule mockitoRule = MockitoJunit.rule(); 
2.常用的Mock打桩方法(设置预期值)如下:

通过设置预期明确Mock对象执行时会发生什么,比如返回特定的值、抛出一个异常、触发一个事件等,又或者调用一定的次数。

方法名 方法描述
thenReturn(T value) 设置要返回的值
thenThrow(Throwable… throwables) 设置要抛出的异常
thenAnswer(Answer<?> answer) 对结果进行拦截
doReturn(Object toBeReturned) 提前设置要返回的值
doThrow(Throwable… toBeThrown) 提前设置要抛出的异常
doAnswer(Answer answer) 提前对结果进行拦截
doCallRealMethod() 调用某一个方法的真实实现
doNothing() 设置void方法什么也不做

例子

//使用then打桩方法
@Test
public void testUserReturn(){
    //使用then
    //注意 !!! when参数必须调用的是method,如果调用的是公有变量,报错误
    Mockito.when(mUser.getUserName()).thenReturn("小明");
    Mockito.when(mUser.getAge()).thenThrow(new NullPointerException("性别不正确"));

    //输出小明
    System.out.println(mUser.getUserName());

    //抛出异常
    System.out.println(mUser.getAge());

}

特别说明:Mock出的对象只能通过以上方法进行设置预期值,如果直接调用对象中的方法去设值,将是无效的。例如:Mock一个List对象,如果调用add方法添加值是无效的。

3.常用验证方法

有时候我们可能不关心返回结果,而是关心方法有否被正确的参数调用过,这时候就应该使用验证方法了。
verify(T mock)验证发生的某些行为 。

方法名 方法描述
after(long millis) 在给定的时间后进行验证
timeout(long millis) 验证方法执行是否超时
atLeast(int minNumberOfInvocations) 至少进行n次验证
atMost(int maxNumberOfInvocations) 至多进行n次验证
description(String description) 验证失败时输出的内容
times(int wantedNumberOfInvocations) 验证调用方法的次数
never() 验证交互没有发生,相当于times(0)
only() 验证方法只被调用一次,相当于times(1)

例子

//after 在给定的时间后进行验证
Mockito.verify(mUser,Mockito.after(1000)).getAge();
//至少验证2次
Mockito.verify(mUser, Mockito.atLeast(2)).getAge();
4.Spy一个真实对象

Mock操作的全是虚拟对象,比如我们Mock一个List,即使设置了Mockito.when(list.get(0)).thenReturn("hello"),实际上list的size大小扔为0。那么我们如果想操作一个真实的对象怎么办呢?
其实Mockito给我们提供了一个对真实对象操作的办法,就是Spy。下面看下例子,就能明白了

@Test
public void testList(){
    List<String> list0 = new ArrayList<>();
    List list = Mockito.spy(list0);
    list.add("1111111");
    list.add("222222222");
    //设置预期值,必须在不越界的情况下设置,否则会越界
    Mockito.when(list.get(0)).thenReturn("hello");
    System.out.println(list.get(0));
    System.out.println(list.get(1));
    System.out.println("list size : " + list.size());
    System.out.println("list0 size : " + list.size());

}

打印的结果如下:

hello
222222222
list size : 2
list0 size : 2
5.Mockito还有其他的用法

可以参考:Mockito框架的使用

如需用到PowerMock参考:PowerMock使用

3.Robolectric框架使用简单说明

Robolectric通过实现一套JVM能运行的Android代码,从而做到脱离Android运行环境进行测试。

1.使用方法
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class MainActivityTest {

}
2.日志输出转换

Java中大多使用System.out.println()输出日志,而在Android中多使用Log。但是在单元测试中无法输出Log的信息,这时我们就要使用ShadowLog进行转换。

@Before
public void setUp(){
    //输出日志
    ShadowLog.stream = System.out;
}

这样 Log日志都将输出在控制面板中。

3.robolectric能做的事情

可以验证Activity、ui组件、service、BroadcastReceiver、Dialog、Toast、Fragment、获取Application、访问资源以及验证Activity和Fragment的生命周期等,功能十分强大,主要使用到ShadowXX做验证。
比如之前例子中的ShadowActivity、ShadowLog、ShadowAlertDialog等。Shadow在实现的同时,拓展了原本的Android代码,实现了许多便于测试的功能,比如例子中用到的 getNextStartedActivity、ShadowToast.getTextOfLatestToast()、ShadowAlertDialog.getLatestAlertDialog()。同时也可以自定义自己的Shadow。

具体例子可以参考:demo工程test目录下的robolectric

4.参考资料

Robolectric使用教程(中文版)

Robolectric doc

四、测试代码覆盖率

1.配置

一般情况下,需要在Android studio中做如下配置才能查看到覆盖率

image
image

2.查看覆盖率

做完上面配置,点击一个测试的类,使用Run xx with Coverage,运行完成就会弹出一个覆盖率界面,如下图:


image
image

覆盖率可以导出,格式是html,可以直接使用浏览器查看,十分方便。

另外也有使用Jacoco做代码覆盖率的,使用起来需要配置gradle,在Activity的oncreate和onDestroy方法中做写入处理,同时还要配置写入权限等,所以个人认为比较麻烦,没用Android studio这个自带的覆盖率使用方便。当然如果有兴趣的可以研究下,提供两个参考地址:Jacoco插件Android studio中使用Jacoco

最后附上例子源码:点击查看

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