Android单元测试那些事儿(一)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/rickenwang/article/details/77601594

Android单元测试基本知识

单元测试简介

在Android Studio上进行单元测试是相对来说比较简单的,主要可以分为两类:

  • local unit test : 本地单元测试
    本地单元测试是跑在本地JVM上的,不依赖于Android设备,所以无法测试依赖于Andorid框架的代码。优点是运行速度快,可以直接访问你电脑上的本地资源。

在运行local unit test时,android.jar将不包含任何实际的代码,如果在运行local unit test时调用了android框架代码,那么将会报

Error: "Method... not mocked"

解决方案有两种:

  • 利用instrumentation test来进行测试

  • 在project build.gradle文件中添加

android {
  ...
  testOptions {
    unitTests.returnDefaultValues = true
  }
}

这样所有的android框架代码的方法均会返回默认值0或者null。当然这并不是一种值得推荐的方法。

Android Studio默认使用JUnit来进行单元测试,但是你也可以Mockito等测试框架来拓展测试能力。

  • instrumentation test : 仪器单元测试
    仪器单元测试是跑在Android设备上的,可以用于测试依赖于Android框架的代码。Android Studio默认采用Espresso来进行测试。

instrumentation test实际上运行了两个app。第一个是被测试的app,第二个是instrumentation test所在的application,androidTest目录下其实包含了一个完整的工程,如manifest文件、资源文件等,只不过这些文件已经完全被隐藏。系统在同一个进程下运行这两个app,因此测试app可以调用app的方法和修改app的变量。

一图胜千言

一般来说,这两种单元测试各有其优缺点,我一般用instrumentation test比较多。

编写单元测试

Android Studio默认已经给我们添加好了单元测试需要的依赖框架,我们只需要在相应的类或方法上,点击Ctrl+Shift+T生成相应的测试类,然后编写相应的测试代码即可。

for example:
新建一个Calculator类

public class Calculator {

    public int add(int a, int b) {

        return a = b;
    }
}

在Calculator类上点击ctrl+shift+T,直接在androidTest/java/目录下生成CalculatorTest.java文件。然后在CalculatorTest.java右键可以直接运行该单元测试文件。

public class CalculatorTest {
    @Test
    public void add() throws Exception {

        Calculator calculator = new Calculator();
        assertEquals(2, calculator.add(1, 1));
    }

}

为什么这里选择androidTest/java而不是test/java目录,因为前者是真正的Android设备环境,使用起来也很方便,并且后续生成测试代码覆盖率报告时也会很方便。

单元测试mock

有时我们想单独对某个模块进行测试,但是这个模块依赖于其他未完成的模块,这时我们可以使用单元测试的mock功能,单元测试的mock也分为两种:

local unit test的mock

常用的local unit test单元测试框架为mockito,使用方法分为如下几个步骤:
1、在module的build.gradle文件中添加mockito依赖

testCompile 'org.mockito:mockito-core:1.10.19'  // 注意这里testCompile和androidTestCompile的区别,testCompile是对local unit test的,而androidTestCompile针对于instrumentation test

2、在测试的定义处添加注解

@RunWith(MockitoJUnitRunner.class)

3、创建mock对象,并在定义处添加@Mock注解
4、构建when()… then()… 语句定义mock的方法和返回值。

@RunWith(MockitoJUnitRunner.class)
public class CalculatorTest {

    @Mock
    Calculator calculator;

    @Test
    public void add() throws Exception {

        when(calculator.add(3,2))
                .thenReturn(4);

        assertEquals(4, calculator.add(1, 2));
    }

}

这里有几个值得注意的事:

  • testCompile ‘org.mockito:mockito-core:1.10.19’,不要写为androidTestCompile ‘org.mockito:mockito-core:1.10.19’,否则会找不到mockito依赖。

  • 如果when…then… 语句中定义的参数和实际调用的不一致,那么如果返回值是基本类型,则返回空,否则返回null。

网络服务的单元测试

一般对于网络服务单元测试有两种选择,一种是对网络服务进行mock,另一种是直接访问网络。这里我更倾向于第二种方式,因为首先mock那么多的网络接口也是一种麻烦事,其次mock的接口的返回情况可能和实际值也会存在一定的偏差。

网络服务一般也可以分为两种,第一种为同步服务,另一种为异步服务。同步服务直接调用接口获取返回值并对返回值进行判断即可。而异步稍微有点麻烦。

要想对异步服务进行测试,我们首先要清楚Android Studio对单元测试代码运行的基本原理。Android Studio采用了一个单线程,(除了@Before和@After),不断调用@Test方法(不保证调用的顺序),一定要注意的是assert方法必须要在该线程中执行才会有效。知道了这点,异步服务的单测也就明朗了。
我们在发送请求后,将单元测试线程阻塞住,等到获取服务器返回的结果后,再释放单元测试线程,并在单元测试线程中执行assert语句即可。

同步网络服务单元测试

@Test
public void createDir() throws Exception {

    CreateDirRequest createDirRequest = new CreateDirRequest();
    CreateDirResult createDirResult = client.createDir(createDirRequest); // 同步网络服务接口
    assertEquals(true, isSuccess(createDirResult)); // 判断网络任务是否正确执行
}

异步网络服务单元测试

    @Test
    public void createDirAsyn() throws Exception {


        final CountDownLatch countDownLatch = new CountDownLatch(1);

        CreateDirRequest createDirRequest = new CreateDirRequest();

        unitTestResult = false; // 用于记录执行结果
        createDirRequest.setListener(new ICmdTaskListener() {
            @Override
            public void onSuccess(COSRequest cosRequest, COSResult cosResult) {

                unitTestResult = isSuccess(cosResult);
                countDownLatch.countDown();
            }

            @Override
            public void onFailed(COSRequest cosRequest, COSResult cosResult) {
                unitTestResult = isSuccess(cosResult) 
                countDownLatch.countDown();
            }
        });

        client.createDir(createDirRequest); // 执行异步任务
        countDownLatch.await(); // 等待任务执行结束
        assertEquals(true, unitTestResult); // 判断执行结果
    }

单元测试相关命令

1、运行local unit test

gradlew testDebugUnitTest

2、运行instrumentation test并生成测试报告

gradlew createDebugCoverageReport

注意想要生成代码覆盖率报告,需要在module的build.gradle文件下添加

buildTypes {

    debug {
        testCoverageEnabled true
    }
}

单元测试常见问题:

如何获取Context

1Context contextnew MockContext();
2Context context = InstrumentationRegistry.getContext(); // 获取测试app的context
3Context context = InstrumentationRegistry.getTargetContext(); // 获取被测试app的context

第一种方式获取的context在调用getSystemService()方法时或报错,而后两种不会。

日志输出

我在Android Studio 2.3版本上运行instrumentation test,并在Android Monitor中不选择任何过滤器,是可以查看日志的。但是同事在Android Studio 2.1版本上却无法打印日志。

文件读取Permission Denied

场景:自己在Android项目下新建了一个module,其中有个方法调用了写文件代码,然后在进行单元测试时总是报:

java.io.FileNotFoundException: /storage/emulated/0/test.txt: open failed: EACCES (Permission denied)

原因:需要在该module上添加权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

其次由于Android6.0的动态权限特性,如果是targetSdkVersion >= 23,且运行单元测试的手机系统大于或者等于Android6.0,需要降低targetSdkVersion或者使用低于Android6.0的手机。

Test run failed: Instrumentation run failed due to ‘Process crashed.’

是否添加了如下依赖:

androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })

junit.framework.AssertionFailedError: No tests found in com.tencent.cos.COSClientTest

是否配置了

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

no test were found

这个错误导致的原因有很多,基本上都是使用或者配置上的错误。

org.mockito.exceptions.misusing.MissingMethodInvocationException:

public class CalculatorTest {
    @Mock
    Calculator calculator;

    @Test
    public void add() throws Exception {

        //calculator = new Calculator();    // 这里不能再初始化

        when(calculator.add(1,2))
                .thenReturn(4);

        calculator = new Calculator();

        assertEquals(4, calculator.add(1, 2));
    }
}

.Instrumentation run failed due to ‘java.lang.NoClassDefFoundError

在单独运行每个测试类时运行正常,但是调用./gradlew connectedAndroidTest命令来运行单元测试就会报如上错误。更奇葩的是自己在api level为16的模拟器上没有这个问题,在api level为23的真机上就会出现这个问题。

这里值得注意的是instrumentation test并不强制需要application module,只包含library module完全可以进行单元测试

检查了一下午,才发现fastjson和testInstrumentationRunner “android.support.test.runner.AndroidJUnitRunner”有冲突,奇葩的是在1.2.35以及之前的版本存在这个问题,1.2.36和1.2.37就会有这个问题。

展开阅读全文

没有更多推荐了,返回首页