好的测试用例常常能让开发效率和质量大大提升,但是代码设计有时候会使测试用例无从下手、难以书写,神烦,很多时候会让开发者忽略做单元测试,又或干脆就懒得写了。在开源界也涌出了很多优秀的单元测试框架,就是为了弥补“单元测试不好写”这个缺陷:
Mockito 强大的模拟工具,能够模拟出无关的依赖模块、行为,并能够验证调用顺序;
Roboletric 在JVM上模拟Android测试环境(即通过单元测试就能测试部分Android代码)。
本文结合这两个库来介绍一下在Android开发中如何写单元测试。
为什么要单元测试?
- 你做了一个很大的底层改动,跑一遍单元测试,哇,全通过了!测试通过让你有信心将代码发布出去;
- 在写测试的时候你会对自己的代码进行思考,每一个操作应用后将会产生什么样的期望后果;
- 在快速开发时,写几个单元测试保证代码质量。
就拿Fresco里面的单元测试举个例子,它测试DraweeView
在attach/detach到Window上时的反应:
上面这段测试用例很简单,但是功能很强大,它保证了这个DraweeView
在View的attach/detach处理这个环节的代码正确。
StackOverflow上有个回答很棒,你只有开始写单元测试后才能体会到它的强大,那么我们来学习一下吧。
Mockito的作用
通常组件之间都会有模块化依赖,但是写单元测试时一般仅涉及一两个组件,若要复现操作环境是非常困难的事情。而Mockito能够轻易Mock一个依赖模块实例,并指定它的行为,把精力用在你想要的那一小个单元的测试。
1. Mock 模拟行为
这是它的精髓之一,可以通过注解@Mock
(语法糖)或者Mockito.mock(Class<T> clazz)
模拟出一个实例,传入的不论是Interface, Abstract class还是普通Class,统统都会mock出一个继承原类、填满hook的新类。注意这个生成的新类是一个空壳,如果需要使用必须指定行为(Stub)。对于未Stub的方法,通常返回null。我们可以写个test来验证一下:
上面这段测试用例中,testOrigin()
会报失败,因为test()
方法被hook后返回了null。testMock()
会成功,因为我们把这个方法Stub住了,让它返回了”testMock”。
除了mock,还有另一种用法:@Spy/spy(Class<T> clazz)
。它可以对一个类的实例(或者含有无参构造函数的类)进行模拟,未指定行为的方法由原类处理,指定行为的类由hook处理。对上面的ExampleClass
,我们可以用Spy来进行一个测试:
这里testOrigin()
和testMockSpy()
都会通过(注意在@Mock
注释下testOrigin()
会失败),因为Spy
只是部分Mock,对没有模拟行为的部分仍返回原结果,但是用起来也会不一样,具体看下文。
模拟行为(Stub)一共有两类做法:
Mockito.when(obj.methodCall()).thenReturn(result);
会检查返回类型;不可用于重复Stub、返回void的函数、Spy作用下mock类的call。Mockito.doReturn(result).when(obj).methodCall();
不会检查返回类型,可重复Stub
之所以说是两类做法,是因为里面的所有return
都可以换成answer
,不仅指定返回,还可以指定methodCall中做的事情:
ps: 如上调用是唯一一种Stub void返回函数的做法。
2. 验证行为
由于Mockito在mock出来的对象中四处都是hook,所以它可以做到一个很棒的功能:验证调用。基于上文的CountClass
你可以写一个如下的简单测试:
3. 题外话
when(obj.methodCall()).thenReturn(something)
时这个方法到底有没有被执行呢?我感到很好奇。于是我试验了一下,发现它确实是被调用了,但是是无法用Mockito.verify
验证到的。你可以跑一下如下测试:
Mockito.when(mTestClass.addCount()).thenAnswer()
这里一共有三步: mTestClass.addCount() -> when() -> thenAnswer()。
由于mTestClass.addCount()
确实被调用了,所以它产生的后果是持久性的,所以后续验证mCountClass.count==2
是对的。但是这里又蛮有意思的,为什么不会被verify?我去翻阅了一下源码,发现了两处相关代码,供大家理解:
3.1 MockHandlerImpl
中处理函数调用:
这里就是所有的mock对象hook处理集中处,它是类似动态代理的处理。
在setInvocationForPotentialStubbing(invocation)
中它将此次调用添加到了一个LinkedList
中。
3.2 OugoingStubbingImpl
中添加Stub
这里将调用记录,也就是后续verify的时候检查的东西抹去了!
所以说Mocktio.when(obj.methodCall()).thenAnswer(answer)
在重复Stub的时候是存在问题的,虽然不会被verify到,但如果Stub的Answer中做了一些持久改变,它会在下次被Stub时的那次调用中生效。多次Stub可以用Mocktio.doReturn(something).when(obj).methodCall()
,这里它在真实的调用之前已经将Stub替换掉了,所以不会出现这个问题。
Roboletric能做的事
Roboletric是另一个优秀的库,它的目标是你能够用单元测试来测试一些Android相关的代码。正常情况下跟Activity、Service有关的测试需要走Instrument测试( https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests.html ),在实机上跑,然而Robolectric可以让你轻松地在电脑上就跑起Android测试。
Robolectric使用了自己的ClassLoader
,它在UnitTest运行时插入了各类Shadow Object
来为Android原生类添加一些hook用于测试。RobolectricTestRunner
会从它上传的org.robolectric:android-all
里面去拿对应的Android SDK。这个原理比较复杂,先不深究了,主要看看用法吧:
1. 基础用法
首先你有一个MainActivity
,它含有R.id.root_view
的一个根View,里面有一个Button
点击可以跳转SecondActivity
。你可以通过以下这段代码来让你的Activity走一遍onCreate->onStart->onResume
,并做一些检查:
这里有几点注意:
- 使用
@RunWith(RobolectricGradleTestRunner.class)
时需要指定@Config(constants = BuildConfig.class)
,它会从/build/intermediates/
目录下找到merge的Manifest、Resource、Asset目录并加载。如果使用@RunWith(RobolectricTestRunner.class)
则需要手动指定@Config(manifest = "...", resourceDir = "...", assetDir = "...")
,manifest
设置的目录base于Unit Test Config里面的”Working Directory”(见下图)
其他值resourceDir
、assetDir
的目录Base于manifest
的父目录。
visible()
可能会令你感到困惑,因为它不属于Activity
生命周期之一,但是执行visibile()
会保证Activity的到Window上(包括初始化DecorView + add到WindowManager),否则findViewById()
、attachToWindow
等都会有问题。关于MultiDex
Robolectric是在JVM上运行代码,根本没有”MultiDex”这回事,如果你的项目里用到了它,需要额外加入一个依赖:
testCompile "org.robolectric:shadows-multidex:${robolectricVersion}"
它hook了MultiDex
这个类,让它在install
的时候啥也不干。
2. 进阶使用:各类Shadow
Shadow是Robolectric里面的Hook类前缀,你可以利用Shadow得到一些Android组件无法获取到的状态,因为有的属性没有变量维护/没有public getter,又或无法获取上次操作结果,这时候就可以使用Shadow来定制一些需要的属性保存,看下面这个例子:
你的Activity里面有一个View(R.id.view), 它将OnClickListener
设置为了Activity
自身,在点击时会发一个广播(action=”com.desmond.androidtest.TestBroadcast”),我们想验证这个情况。但是有两个问题:
- View无法获取到
OnClickListener
,只能知道hasOnClickListener()
; - Receiver不是你定义的,你只负责发,你如果需要验证需要自己额外注册一个Receiver,并编译运行检查是否成功。
使用Robolectric,你可以写下这么一个测试:
很有意思吧,更多的”Shadow”可以在Shadows
这个类里面找着,用的时候直接Shadows.shadowOf(object)
就可以返回对应Shadow
实例。你还可以自定义Shadow( http://robolectric.org/custom-shadows/ )来完成你需要的测试功能点。
欢迎加入QQ群:568863373。
欢迎关注我们的公众号:魔都三帅
,欢迎大家来投稿~