单元测试目前的地位比较尴尬,你问一个软件开发“单元测试重要么?”大家都说重要,但在真正生产环境中使用的团队少之又少。为什么会出现这种情况呢?总结了下主要有两个原因:
1. 开发任务紧张,敏捷开发没有时间去做单元测试,影响开发效率
2. Android开发单元测试不好做,不知如何去做
对于问题1,Martin Fowler在《重构》里面还解释了为什么单元测试可以节省时间:“我们写程序的时候,其实大部分时间不是花在写代码上面,而是花在debug上面,是花在找出问题到底出在哪上面,而单元测试可以最快的发现你的新代码哪里不work,这样你就可以很快的定位到问题所在,然后给以及时的解决,这反而能提高工作效率”
问题2正是我这篇分享的内容了,让我们一起去做好Android单元测试。
Android单元测试分为两类:
Local unit tests(本地单元测试)
测试在计算机的本地 Java 虚拟机 (JVM) 上运行。 当您的测试没有 Android 框架依赖项或当您可以模拟 Android 框架依赖项时,可以利用这些测试来尽量缩短执行时间。
Instrumented tests(仪器测试)
测试在设备或模拟器上运行。 这些测试有权访问 Instrumentation API,让您可以获取某些信息(例如您要测试的应用的 Context), 并且允许您通过测试代码来控制受测应用。 可以在编写集成和功能 UI 测试来自动化用户交互时,或者在测试具有模拟对象无法满足的 Android 依赖项时使用这些测试。
做为这个系列的第一篇,我们主要来聊聊本地单元测试
一个很重要的点是:“我们什么情况下使用本地单元测试?” 有以下两个准则
1. 测试类是pure java类
2. 测试类简单依赖Android环境
使用Android studio创建新项目时会自动帮你创建本地测试文件目录module-name/src/test/java/.
在本地单元测试中使用最广泛的是JUnit 4,Mockito,Robolectric 我们分别介绍一下:
JUnit是在Java中使用最为广泛的单元测试库,其他大部分测试框架基于或兼容JUnit,我们看看在Android中是如何使用的
dependencies {
testCompile 'junit:junit:4.12'
}
我们有个待测试的类Calculator
public class Calculator {
public int add(int one, int another) {
return one + another;
}
public int multiply(int one, int another) {
return one * another;
}
}
我们现在需要对add()
和multiply()
进行单元测试,有两种方式生成测试类:
1. 我们在目录module-name/src/test/java/.
新建对应测试类
2. 我们在待测试类Calculator上使用快捷键CTRL+SHIFT+T一键生成测试类
AndroidStudio上推荐使用第二种方式,会很方便。勾选相应的测试类及setup与tearDown即可生成
public class CalculatorTest {
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void add() throws Exception {
}
@Test
public void multiply() throws Exception {
}
}
普通的Java类,但是方法上会有相应的注解,我们这里来讲解下这些注解:
@Before:初始化方法 对于每一个测试方法都要执行一次(注意与BeforeClass区别,后者是对于所有方法执行一次)
@After:释放资源 对于每一个测试方法都要执行一次(注意与AfterClass区别,后者是对于所有方法执行一次)
@Test:测试方法,在这里可以测试期望异常和超时时间
@Test(expected=ArithmeticException.class)检查被测方法是否抛出ArithmeticException异常
@Ignore:忽略的测试方法
@BeforeClass:针对所有测试,只执行一次,且必须为static void
@AfterClass:针对所有测试,只执行一次,且必须为static void
我们验证下执行顺序:
public class CalculatorTest2 {
@BeforeClass
public static void beforeClass() {
System.out.println("@BeforeClass");
}
@Before
public void setUp() throws Exception {
System.out.println("@Before");
}
@After
public void tearDown() throws Exception {
System.out.println("@After");
}
@Test
public void add() throws Exception {
System.out.println("@Test add()");
}
@Test
public void multiply() throws Exception {
System.out.println("@Test multiply()");
}
@AfterClass
public static void afterClass() {
System.out.println("@AfterClass");
}
}
@BeforeClass
@Before
@Test add()
@After
@Before
@Test multiply()
@After
@AfterClass
JUnit另一个重要的知识点就是Assert(断言),JUnit提供了一系列assertXXX方法用来对预期与实际结果进行测试,内容比较简单,具体可以参考JUnit官方文档 Assertions CN
我们有了这些基础知识,就可以开始进行单元测试了
public class CalculatorTest {
Calculator mCalculator;
@Before
public void before() {
/**
* @Before 一般设置测试的前置条件
*/
mCalculator = new Calculator();
}
@Test
public void add() {
Assert.assertEquals(4, mCalculator.add(1, 3));
}
@Test
public void multiply() {
Assert.assertEquals(5, mCalculator.multiply(1, 5));
}
@After
public void after() {
}
}
在相应的方法上点击就能直接运行单元测试,也可以在测试类上点击运行,一次运行多个测试方法。
整个流程,我们可以发现使用JUnit结合AndroidStudio快捷键可以很快完成一次单元测试。
优点:执行速度快,一般用于工具类或Pure Java类(MVP架构中的Presenter层等)
缺点:只能测试有明确返回值的方法,对void方法无法测试;对存在复杂依赖的类无法测试
Mockito是什么?Mockito是一套非常强大的测试框架,被广泛的应用于Java程序的unit test中。Mockito可以很好的解决JUnit4的两个缺点。它主要能做到以下两点:
1. 验证对象方法调用的情况
2. mock(模拟/代理/替换)对象方法的行为
dependencies {
testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:1.10.19"
}
我们新建一个测试类MockTest
,这里有Mockito的最重要的几个API例子,代码中有详细注释
public class MockTest {
@Test
public void testMock() throws Exception {
// 使用Mockito.mock(xxx) 创建一个Mock对象
ArrayList mockedList = Mockito.mock(ArrayList.class);
// 通过Mockito.when(xxx).thenXXX();可以改变对象方法的行为
// 语义理解是当调用mockedList.get(0)时返回"first"
Mockito.when(mockedList.get(0)).thenReturn("first");
// 控制台打印出 "first"
// 实际上 mockedList是一个空对象,我们并没有添加任何元素,这就是Mockito改变对象行为的能力
System.out.println(mockedList.get(0));
// 对于没有stubbed的方法会返回默认值,(e.g: 0,false, ... for int/Integer, boolean/Boolean, ...)
System.out.println(mockedList.get(999));
// 验证list.get(0)是否调用
Mockito.verify(mockedList).get(0);
// 验证list.get(2)没有调用
Mockito.verify(mockedList, Mockito.never()).get(2);
// 验证list.get(int)调用次数;Mockito.提供了很多anyXXX方法,在你不关心参数的情况下可以直接使用
Mockito.verify(mockedList, Mockito.times(2)).get(Mockito.anyInt());
}
}
我们再来整理一下:
1. 通过Mockito.mock(xxx) 创建一个Mock对象
2. 通过Mockito.when(xxx).thenXXX();可以(指定/替换)对象方法的行为
3. 通过Mockito.verify(xxx).someMethod();验证该方法是否被调用
另一个与Mockito.mock(xxx)相似的重要方法是Mockito.spy(xxx),因为都会创建一个Mock对象很多人会弄不清它们的区别,我们通过代码来看一下:
@Test
public void testSpy() {
ArrayList mockObj = Mockito.mock(ArrayList.class);
mockObj.add("one");
mockObj.add("two");
// Mockito.mock创建的对象对没有stubbed的方法返回默认值
// print 0
System.out.println(mockObj.size());
// print null
System.out.println(mockObj.get(0));
// Mockito.spy 生成的对象,执行中会调用对象实现方法;mock则是返回默认值,不会调用实现
List spyObj = Mockito.spy(ArrayList.class);
spyObj.add("one");
spyObj.add("two");
// Mockito.spy创建的对象会调用真实的方法
// print 2
System.out.println(spyObj.size());
// print "one"
System.out.println(spyObj.get(0));
// 因为spyObj.get(2)会真实调用,会抛出异常throw IndexOutOfBoundsException
// Mockito.when(spyObj.get(2)).thenReturn("first");
Mockito.doReturn("first").when(spyObj).get(2);
System.out.println(spyObj.get(2));
}
整理一下:
1. Mockito.mock创建的对象对没有stubbed的方法返回默认值,不会调用实现
2. Mockito.spy 生成的对象,执行中会调用对象实现方法
3. 当你需要关心对象实现细节时使用#spy;当你只想知道对象方法调用情况使用#mock就能满足需求
我们在开发中经常会使用到回调的方式,那这种情况该如何通过Mockito模拟呢?我们用登录的例子来讲解,一起看看以下代码:
// 登录管理类
public class LoginMgr {
/**
* 访问服务器进行登录验证
*/
private LoginApi mLoginApi;
public LoginMgr(LoginApi loginApi) {
mLoginApi = loginApi;
}
public void login(String name, String password) {
mLoginApi.login(name, password, new LoginCallback() {
@Override
public void onSuccess() {
loginSuccess();
}
@Override
public void onFailed() {
loginFail();
}
});
}
public void loginSuccess() {
System.out.println("LoginMgr.loginSuccess");
}
public void loginFail() {
System.out.println("LoginMgr.loginFail");
}
}
// 登录回调接口
public interface LoginCallback {
void onSuccess();
void onFailed();
}
我们现在要对login
方法进行测试,验证流程
1. LoginMgr登录成功调用loginSuccess();
2. LoginMgr登录失败调用loginFail();
public class LoginMgrTest {
private LoginApi mLoginApi;
private LoginMgr mLoginMgr;
private ArgumentCaptor mArgumentCaptor;
@Before
public void setUp() throws Exception {
// 我们关注LoginApi#login是否得到调用,使用mock
mLoginApi = Mockito.mock(LoginApi.class);
// 我们关注LoginMgr#login需要真正执行,使用spy
mLoginMgr = Mockito.spy(new LoginMgr(mLoginApi));
// 通过ArgumentCaptor捕获传递到LoginApi#login的回调
mArgumentCaptor = ArgumentCaptor.forClass(LoginCallback.class);
}
@Test
public void testLoginSuccess() {
// 调用登录方法
mLoginMgr.login("lee", "123");
// 验证LoginApi#login是否执行
Mockito.verify(mLoginApi).login(Mockito.anyString(), Mockito.anyString(), mArgumentCaptor.capture());
// 调用捕获到的回调onSuccess函数
mArgumentCaptor.getValue().onSuccess();
// 验证LoginMgr 回调中loginSuccess是否执行
Mockito.verify(mLoginMgr).loginSuccess();
// 流程验证无问题
}
@Test
public void testLoginFailed() {
mLoginMgr.login("lee", "123");
Mockito.verify(mLoginApi).login(Mockito.anyString(), Mockito.anyString(), mArgumentCaptor.capture());
mArgumentCaptor.getValue().onFailed();
Mockito.verify(mLoginMgr).loginFail();
}
}
总结一下:
1. 通过ArgumentCaptor#capture()可以捕获传递的回调
2. 通过ArgumentCaptor控制回调的流程
Mockito对大部分API支持注解,极大的减少了重复代码,增强可读性,易于排查错误。
我们通过例子来了解如何使用这些注解:
@RunWith(MockitoJUnitRunner.class)
public class LoginMgrAnnotationTest {
/**
* @Mock
* @Spy
* @Captor
*
* 1.方便对象的创建
* 2.减少对象创建的重复代码
* 3.提高测试代码可读性
*/
@org.mockito.Mock
private LoginApi mLoginApi;
/**
* @InjectMocks
*
* 通过这个注解,可实现自动注入mock对象。
* Mockito首先尝试类型注入,如果有多个类型相同的mock对象,那么它会根据名称进行注入
*
* 自动注入了LoginApi对象到LoginMgr
*/
@Spy @InjectMocks
private LoginMgr mLoginMgr;
@Captor
private ArgumentCaptor mArgumentCaptor;
@Before
public void setUp() throws Exception {
/**
* 使用Mock注解
* 1. MockitoAnnotations.initMocks(testClass);
* 2. 测试类开头@RunWith(MockitoJUnitRunner.class)
*/
// MockitoAnnotations.initMocks(this);
}
@Test
public void testLoginSuccess() {
// 调用登录方法
mLoginMgr.login("lee", "123");
// 验证LoginApi#login是否执行
Mockito.verify(mLoginApi).login(Mockito.anyString(), Mockito.anyString(), mArgumentCaptor.capture());
// 调用捕获到的回调onSuccess函数
mArgumentCaptor.getValue().onSuccess();
// 验证LoginMgr 回调中loginSuccess是否执行
Mockito.verify(mLoginMgr).loginSuccess();
// 流程验证无问题
}
}
Context是Android中特有的,我们Android中很多方法都会通过Context,你会发现你无法新建一个Context类,调用相应的方法也会抛出异常,因为默认情况下,Android Gradle for Gradle会针对android.jar库的修改版本执行本地单元测试,该库不包含任何实际代码(这些API仅由设备上的Android系统映像提供)。
从你的单元测试中调用Android类的方法会引发异常,这是为了确保您只测试您的代码,并且不依赖于Android平台的任何特定行为。
我们在单元测试就会很棘手,Mockito能很好的帮我们解决这个问题,我们看以下代码:
// 待测试的类
public class Mock {
public String getString(Context context) {
String str = context.getResources().getString(R.string.app_name);
System.out.println(str);
return str;
}
}
@RunWith(MockitoJUnitRunner.class)
public class MockTest {
@Mock
Context mContext;
@Mock
Resources mResources;
@Test
public void getString() {
// Gradle运行Local Unit Test 所使用的android.jar里面所有API都是空实现,并抛出异常的
// 只有apk安装到终端/模拟器 这些才是真正的实现
Mockito.when(mContext.getResources()).thenReturn(mResources);
Mockito.doReturn("mock string").when(mResources).getString(Mockito.anyInt());
String result = mMock.getString(mContext);
Assert.assertEquals(result, "mock string");
}
}
这样我们可以避免调用Android代码发生的异常。
到这里我们基本把Mockito框架介绍完了,Mockito结合JUnit4基本可以完成对任一对象进行单元测试的要求了。尤其是在使用MVP架构对P层的测试中,自动注入view module的mock对象,可以测试任何路径了。当然Mockito还是有它的缺陷:
1. Mockito无法对final类型、private类型以及静态类型的方法进行mock。
2. Android的支持还不够好
针对上面的问题1,可以使用 powermock 解决,因为和Mockito相似,这里就不多介绍了,有需要的同学请自行前往阅读。
接下来我们要介绍神器“Robolectric”就是为Android而生的单元测试框架,由于篇幅问题分为两部分,未完待续……
Fundamentals of Testing
Android单元测试之JUnit4
robolectric
使用Robolectric进行Android单元测试
Android 集成Robolectric下的一些坑
Robolectric使用教程