• Android单元测试之Local unit tests(上)
      • 简介
      • 本地单元测试
        • JUnit 4
          • 添加依赖
          • 测试例子
          • 结论
        • Mockito
          • 添加依赖
          • 测试例子-mock基本使用
          • 测试例子-mock与spy区别
          • 测试例子-异步任务回调
          • 测试例子-mock注解
          • 测试例子-mock Android dependencies
          • 结论
      • 未完待续
      • 参考链接

Android单元测试之Local unit tests(上)

单元测试目前的地位比较尴尬,你问一个软件开发“单元测试重要么?”大家都说重要,但在真正生产环境中使用的团队少之又少。为什么会出现这种情况呢?总结了下主要有两个原因:
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 4

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是什么?Mockito是一套非常强大的测试框架,被广泛的应用于Java程序的unit test中。Mockito可以很好的解决JUnit4的两个缺点。它主要能做到以下两点:
1. 验证对象方法调用的情况
2. mock(模拟/代理/替换)对象方法的行为

添加依赖
dependencies {    testCompile 'junit:junit:4.12'    testCompile "org.mockito:mockito-core:1.10.19"}
测试例子-mock基本使用

我们新建一个测试类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();验证该方法是否被调用

测试例子-mock与spy区别

另一个与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控制回调的流程

测试例子-mock注解

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();        // 流程验证无问题    }}
测试例子-mock Android dependencies

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使用教程

更多相关文章

  1. Android 解决65535的限制 使用android-support-multidex解决Dex
  2. Android 应用语言切换的三种方法
  3. Android 中的Parcelable序列化对象
  4. Android CheckBox中设置padding无效问题解决方法
  5. Android设备Root检测方法
  6. Android Studio中创建Selector文件的方法
  7. blcr加速android启动速度遇到的问题及解决方法
  8. Android 公共库的建立方法

随机推荐

  1. Android(安卓)studio嵌入二维码扫描
  2. Android一次刷机
  3. [4.18]Android生命周期介绍
  4. android:launchMode="singleTask" 与 onN
  5. Android(安卓)service 实现过程
  6. Android(安卓)Rom签名文件的生成与签名
  7. android 暗码
  8. FAQ00366]如何使Android应用程序获取系统
  9. Android(安卓)-Recovery
  10. Android(安卓)RadioGroup设置默认选中项