Android单元测试实践

Posted by smallSohoSolo on April 19, 2017

为什么要引入单元测试

一般来说我们都不会写单元测试,为什么呢?因为要写多余的代码,而且还要进行一些学习,入门有些门槛,所以一般在工程中都不会写单元测试。那么为什么我决定要写单元测试。因为两个条件

  1. 我很懒:我每次改完都很懒测试
  2. 我很怂:我要是不测试,没有一次通过的信心,于是我还是要测试。。。

这篇博客看完并不会让你完全掌握单元测试,但是会给你在单元测试的开始有一个好的指引

大大提高工作效率

单元的概念比较模糊,可以是一个方法,可以是一个时机,但是不是一整套环节,一整套环节那就是集成测试了。为什么说大大提高了工作效率。有以下几个场景

  1. 一个计算数字的方法你发现你写错了,你把+写成了*,处于责任心,测试一下。首先你点击build,然后登上几分钟,期间可以喝个茶,看个朋友圈。。。。。然后,你发现你写成了/,再来一次吧少年。。。。
  2. 你要修改一个项目,这时候你改了一个函数的小细节,线上crash了。。。
  3. 你在原来的基础上改了一下业务,有一个一万人只有一个人用的场景你不知道,UI出Bug了

等等等等,这些场景在普通的测试中无法很好的发现,毕竟你不能让QA每次都把之前的业务都测试一下。所以这种场景如何解决?

写单元测试

你可以测试一个方法而不用再跑一次程序,项目写大了build一次可是很耗时的。写好的单元测试也不用删除,ci会帮你每次运行一下,来保证之前的逻辑是没有问题的。

单元测试框架的选择

Android的单元测试框架林林总总,目测10多种,所以,挑选是个问题,我总认为google官方推荐的和大众都选择使用的较好。所以我推荐(放心用,绝对是主流套装)

    //local test
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:2.7.22'
    testCompile 'org.robolectric:robolectric:3.3.2'
    //android test
    androidTestCompile 'org.mockito:mockito-core:2.7.22'
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
  1. JUnit4:基础的单元测试使用
  2. espresso:UI测试使用
  3. Mockito:Mock你要测试的类,
  4. Robolectric(可选):让你在JVM环境中模拟Android的环境,这里稍后解释为什么可选

解释一下依赖,首先测试分成两种

  1. 本地单元测试: 位于 module-name/src/test/java/

不需要Android环境的测试用例写在这里

  1. Android Instrumentation测试: 位于 module-name/src/androidTest/java/。

需要使用Android环境的测试用例写在这里,例如你要使用TextUtils.isEmpty(“”),这个函数是android包中的,需要写在这个包下,写在上面运行时会报错。

引用自 Android官方文档

依次解释

  • JUnit4是写测试用例的,所有的测试用例都是用JUnit4来写,所以这是基础库,不解释。
  • espresso是Google官方的UI测试库,可以对UI做白盒测试
  • Mockito是用来做Mock的,并且Mockito2.6+已经支持Android Instrumentation下使用什么是Mock?
  • Robolectric是可以在JVM上模拟Android环境,也就是你可以在本地单元测试中使用这个库模拟Android环境来做到调用Android Api的测试,为什么要用呢?因为可能你在CI中进行测试你无法让系统开启一个Android模拟器或者真机来进行仪器测试,这也就是为什么这个库可选,你如果不用这个场景,就是不启动虚拟机才测试,你也就不用引入这个库。而且打包使用真机过程比较漫长,使用这个直接在JVM上跑速度更快

常见的单元测试

本文不能把所有的情况都介绍到,也不能将所有的框架都介绍完全,挑选几个主要的例子介绍来让大家理解总体上的结构,然后自己挑选侧重点根据文档深入的学习。

本地单元测试

Example:验证一个数组的长度是否等于5,并且是否调用了add(1)这个操作,使用local test完成

public class ExampleUnitTest {

    @Test
    public void addition_isCorrect() throws Exception {
        List<Integer> mockList = Mockito.mock(List.class);
        mockList.add(1);
        Mockito.when(mockList.size()).thenReturn(5);
        Mockito.verify(mockList).add(1);
        assertEquals(5,mockList.size());
    }

}

首先我们知道一个问题,mockito所mock出来的对象都是假的,是的,没错,你用了一个假数组,如果想要用真的,老老实实new一个,也就是说针对一个数组,所有的操作都是拿不到正常结果的,除非你想我上面那种方式去指定他。你如果执行mockList.size(),返回的是0,最后一句的作用是判断两个值是否相等。Mockito.verify是验证函数,可以验证这个方法是否被执行过。

Example:这次不用假数组了,用真的!然后我们偷梁换柱,就是部分mock

public class Example2UnitTest {

    @Test
    public void spyText() {
        List<Integer> mockList = new ArrayList<>();
        mockList = Mockito.spy(mockList); //如果直接spy
        mockList.add(1);
        mockList.add(2);
        Mockito.when(mockList.size()).thenReturn(5);
        assertEquals(5, mockList.size());
        assertEquals(mockList.get(0) == 1, true);
        Mockito.when(mockList.size()).thenCallRealMethod();
        assertEquals(2, mockList.size());
    }

}

首先区分一下spy一个对象和spy一个类的区别:如果我此处调用Mockito.spy(List.class),那么你得到的list仍然是假的,原因是看源码。

    @Incubating
    public static <T> T spy(Class<T> classToSpy) {
       return MOCKITO_CORE.mock(classToSpy, withSettings()
               .useConstructor()
               .defaultAnswer(CALLS_REAL_METHODS));
    }

很清晰,spy一个类是创建一个类的mock对象,然后调用一个mock类的真实返回,mock得到的类就是假的类,所以她的真实的返回值也是假的。所以我们此处spy一个对象。

spy一个对象是默认的对象的返回都使用真实数据返回,如果你进行修改,那么就用你修改的类来做返回。这样就能做到部分修改。我们还调用了这个Mockito.when(mockList.size()).thenCallRealMethod();这句的意思很清晰,就是使用这个函数的真实返回值,就算是一个mock出来的“假对象”,仍然可以用这个的到真实的返回值。

Android Instrumentation测试

Android Instrumentation测试跟本地测试区别不大,唯一的区别是运行的时候需要我们选择一个设备,然后gradle会打包测试用的Apk在手机上运行测试用例,如果想打log,换成Log.d等方法在Android Monitor中查看

1.Example: 查看当前的context是不是我的包名

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() throws Exception {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();
        assertEquals("com.ss.android.ies.mobcase.test", appContext.getPackageName());
    }
}

这是一个官方的例子,在androidTestCompile中,我们可以使用Api来发现一个当前的appContext,这在jvm中是做不到的。因为你没有android环境。这里判断一下是否一样。

2.Example: 使用espresso在UI上进行测试

androidTextCompile的优点是可以在android环境上进行测试,所以少不了UI的白盒测试,这里举一个例子,测试点击button之后editText的文字是否是123456

@RunWith(AndroidJUnit4.class)
public class TestActivityTest {

    @Rule
    public ActivityTestRule<TestActivity> activityTestRule = new ActivityTestRule<>(TestActivity.class);

    @Test
    public void buttonTest() {
        Espresso.onView(withId(R.id.button)).perform(click());
        Espresso.onView(withId(R.id.edit))
                .check(matches(withText("123456")));
    }

}

此处很好理解,espresso的语法跟自然语言一样,写单元测试分为三个步骤

  • 执行
  • 检查

很简单的几句语法就能对UI进行测试,这里我们进行了对button进行定位,然后执行click操作。第二部进行检查,因为我的逻辑是点击按钮editText.setText(“123456”),所以这个单元测试是通过的。

Thanks!

简单的对以上几个框架进行了介绍,感谢以下教程。

文档链接