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

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

你可能感兴趣的:(android)