Android单元测试
今日在做微信IoT打印机的项目,其中涉及到用Android程序访问WPS服务来实现将word文档另存为PDF的功能。于是,秉着TDD的精神,开始学习如何在Android中做单元测试。
入门
JUnit
首先看到的是Android的官网文档Getting Started with Testing, Building Local Unit Tests。有两种类型的测试,一种是跑在主机上的,叫Local Unit Test,另一种跑在目标机上,叫Instrumented Unit Test。
Local Unit Test基于JUnit,上面的关于Local Unit Test的链接已经有了如何在Android Studio中使能JUnit的范例了。配置好Dependencies就可以了。Android Studio会去maven库中抓取JUnit的jar包并导入到项目中,之后就可以使用了。
遇到错误Error:(23, 17)"Failed to resolve: junit:junit:4.12
,在build.gradle下面加
repositories {
maven { url 'http://repo1.maven.org/maven2' }
jcenter { url "http://jcenter.bintray.com/" }
}
或者自行去下载junit-4.12.jar,再放到libs目录底下就可以了。
Mockito
在C++中做Unit Test的时候,我们往往需要自己构建stub,来隔离需要测试的code。在Java中,我们有现成的框架可以用,这就是Mockito。网上关于Mockito+Android单元测试的文章一搜一大摞。我也简单的摘录了几个。
- Android测试二:Mockito与单元测试
- Android 单元测试之JUnit和Mockito
- Android最佳Mock单元测试方案:Junit + Mockito + Powermock
Mockito的主要作用就是模拟外部类的行为,达到隔离待测代码的目的。如下图。
import android.os.classA
class B {
public void methodC (){
classA a = new classA();
a.methodA();
}
}
请问这里我要怎么在methodC中模拟对象A的行为。诸如此类很多问题,会在下面一章进行罗列。于是在Google之中就知道了PowerMock。
PowerMock
PowerMock是Mock框架的扩展,他同时支持EasyMock和Mockito。网上的文章也是千千万,具体测试语句的写法也是各不相同,所以最靠谱的还是官网。PowerMock提供了以下模拟的途径:
- 模拟构造函数
- 模拟静态类
- 突破访问权限控制,操纵私有成员,私有静态成员
- 调用私有方法。
进阶
在实际的编码过程中,遇到很多问题,Google了很久,现将所有问题以及解决方案罗列一下:
对static方法的模拟
Looper mockLooper = mock(Looper.class);
PowerMockito.mockStatic(Looper.class);
PowerMockito.when(Looper.class, "getMainLooper").thenReturn(mockLooper);
对构造函数的模拟
解决上一章提到的问题,可以用模拟classA的构造函数来解决,同样是用到PowerMockito
PowerMockito.whenNew(Intent.class).withNoArguments().thenReturn(mockIntent);
PowerMockito.whenNew(Intent.class).withArguments(anyString()).thenReturn(mockIntent);
针对有参数和没有参数的两种情况
操作private成员
// 操作私有非静态成员
Whitebox.setInternalState(mockObj, "mPrivateStaticVariable", value);
Whitebox.getInternalState(mockObj, "mPrivateStaticVariable");
// 操作私有静态成员
Whitebox.setInternalState(xxx.class, "mPrivateStaticVariable", value, xxx.class);
Whitebox.getInternalState(xxx.class, "mPrivateStaticVariable", xxx.class);
局部模拟
这个会用到Mockito中的spy关键字。在很多情况下,我们无法剥离需要模拟的类和待测试的类。例如,实际中用到的下面的类:
public class ConverterFacade extends Service{
private void bindOfficeService(){
if (bindService(intent, connection, BIND_AUTO_CREATE)) {
...
}
}
}
ConverterFacade继承自Android自有的类Service,Service继承自Context,bindService是Context接口的一个方法。如果不对之进行mock,测试的时候会报错。因为我们要测试ConverterFacade,又不能将之mock掉,所以只能退而求其次,采用partial mock。插入官网的介绍,Mockito.spy。官方并不推荐使用spy,因为这样会让模拟的code和真实的code混在一起。
Real spies should be used carefully and occasionally.
例子:
List list = new LinkedList();
List spy = spy(list);
//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
//using the spy calls real methods
spy.add("one");
spy.add("two");
//prints "one" - the first element of a list
System.out.println(spy.get(0));
//size() method was stubbed - 100 is printed
System.out.println(spy.size());
//optionally, you can verify
verify(spy).add("one");
verify(spy).add("two");
为什么要用doReturn().when()
而不能用when().doReturn()
//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
when(spy.get(0)).thenReturn("foo");
//You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);
java.lang.NoClassDefFoundError
遇到了这个问题,原因是没有导入moffice-service-base包。在这里弄好就行了
java.lang.ClassNotFoundException
This exception indicates that the class was not found on the classpath. This indicates that we were trying to load the class definition, and the class did not exist on the classpath.
java.lang.NoClassDefFoundError
This exception indicates that the JVM looked in its internal class definition data structure for the definition of a class and did not find it. This is different than saying that it could not be loaded from the classpath. Usually this indicates that we previously attempted to load a class from the classpath, but it failed for some reason - now we’re trying to use the class again (and thus need to load it, since it failed last time), but we’re not even going to try to load it, because we failed loading it earlier (and reasonably suspect that we would fail again). The earlier failure could be a ClassNotFoundException or an ExceptionInInitializerError (indicating a failure in the static initialization block) or any number of other problems. The point is, a NoClassDefFoundError is not necessarily a classpath problem.
mockStatic导致内部类无法有效引用外部类的成员
解决方法是,不要mockStatic(ConverterFacade.class);
要mockStatic(ConverterFacade.class, CALLS_REAL_METHODS);
捕获参数
Message m = mock(Message.class);
m.what = 1;
Handler mHandler = mock(Handler.class);
mHandler.sendMessage(m);
m.what = 2;
mHandler.sendMessage(m);
ArgumentCaptor<Message> argumentCaptor2 = ArgumentCaptor.forClass(Message.class);
verify(mHandler, times(2)).sendMessage(argumentCaptor2.capture());
List<Message> msgList = argumentCaptor2.getAllValues();
ArgumentCaptor可以帮助捕获参数调用情况。argumentCaptor.getValue()
可以捕获最后一次调用的参数,argumentCaptor.getAllValues()
可以捕获多次调用或者一次调用传递多个参数的情况。
这其中牵涉到Java对象传递的方法。如上程序代码,虽然最终得到的List的size是2,但是这两个表项的what值都指向2,用Android Studio查看对象地址,发现这俩都指向同一个对象,所以其值相同也就不难理解了。如果想要捕获到两个不同的值,就要new两次。