Skip to content

Instantly share code, notes, and snippets.

@xdtianyu
Created August 1, 2017 08:07
Show Gist options
  • Save xdtianyu/1ab43c109fb84eca62de4dd47cb7ca5f to your computer and use it in GitHub Desktop.
Save xdtianyu/1ab43c109fb84eca62de4dd47cb7ca5f to your computer and use it in GitHub Desktop.

Android 开发人员自动化测试

[TOC]

​ 编写测试的目的是为了验证程序是否正确执行、行为无误及是否稳定可用。同时,拥有充分测试代码的项目易于维护,便于交接、团队协作。

​ 有很多的 Android 架构如 MVP、Clean、DataBinding 等,这些架构的一个共同点是分离视图界面、数据模型、业务/控制逻辑,最大限度的降低代码耦合,减少函数/方法的副作用。网上介绍这些架构、框架的文章很多,但是很少有提及为什么要这么做。使用规范的代码架构,降低代码耦合,直观上可以使代码易于维护,提高代码利用率、便于重构,其实最直接的作用是为了方便的写测试代码,这一点却很少有人提及。

​ 以 MVP 架构举例,将 APP 代码分离为三个层次:Model, View, Presenter。Model 层封装数据模型,不做任何逻辑操作,View 层即 Activity、Fragment 等只用来展示界面、返回界面事件,Presenter 层来和数据交互,控制界面显示逻辑,响应界面事件及数据变化。View 不直接和 Model 交互,同时 Model 和 Presenter 不依赖于 Android 系统库的代码,这样可以方便的在本地电脑对 Presenter 和 Model 做单元测试。

​ 本文档主要是对 Android 开发人员如何编写单元测试及 UI 自动化测试代码的整理,其中大部分篇幅是对 Android 官方文档翻译,要阅读英文原文,请点击每小节的参考链接或文档最后的参考资料链接。

## 一. 测试类型

​ Android 测试类型可以分为三大类:单元测试、集成测试、压力测试。单元测试又可以分为本地单元测试、仪表 (Instrumented) 单元测试;集成测试分为应用内、跨应用测试;压力测试可以分为 Monkey 、Monkey Runner。除了压力测试中的 Monkey 测试,其他五类测试都需要开发人员编写测试代码。

​ 其中 Monkey 测试不需要开发人员编写测试代码,而 Monkey Runner 是 Python 程序,一般 Android 开发人员不直接编写此类测试代码。由于篇幅关系,本文不讨论压力测试的内容。

类型 子类型 描述
单元测试 本地单元测试 运行在本地 Java 虚拟机,不依赖于 Android 框架或者可以通过 Mock 来 模拟 Android 框架的测试。因为不依赖与 Android 框架及模拟器或开发机,执行快
Instrumented 单元测试 运行于开发机或模拟器,这些测试可以访问应用的运行状态信息,如应用的 Context。用这些测试来测试不能通过Mock模拟而必须依赖于 Android 框架的组件。因为要运行在真实的 Android 环境下,执行较慢。
集成测试 仅应用内组件测试 这类测试用来确认目标应用在用户执行某特定操作或输入时,能够正确响应。例如,可以用来确认用户与应用交互时响应了正确的用户界面。UI 测试框架如 Espresso 允许开发者程序化的模仿用户行为并验证复杂的程序内用户交互。
跨应用组件测试 这类测试用来验证用户在多个不同应用之间或应用与系统交互时是否正确。例如可以通过这类测试来验证用户操作后系统状态栏的通知是否正确,也可以通过这类测试来点击状态栏通知、点击权限确认按钮。UI 测试框架如 UI Automator 可以用来创建此类测试。
压力测试 Monkey Monkey 是一个命令行工具,可以通过 adb 接口随机触摸、点击、按键或触发手势。它可以循环执行这些随机的触摸等事件,并返回遇到的错误。
monkeyrunner monkeyrunner 是一个 Python 程序,是一个测试 API 及运行环境。API 包括 连接设备、安装/卸载软件包、截取屏幕、对比两个图片及运行测试软件包等。使用这个 API 可以编写强大复杂的测试。

二. 测试支持库

Android 官方的测试支持库包括 AndroidJUnit4,Espresso,UI Automator 。其中 Espresso 和 UI Automator 可以结合在一起使用,来模拟完整的用户行为。

1. AndroidJUnit4

​ AndroidJUnit4 是 Android 单元测试的主要方法,在本地计算机或持续集成环境中运行,不需要测试机或者模拟器。

2. Espresso

​ Espresso 是用来测试 Android 应用界面的主要方法,运行于测试机或者持续集成环境中的模拟器。不同于 UI Automator,Espresso 只能用来测试应用本身的界面,但是 Espresso 更加便捷易用,可以非常轻松的实现点击、滑动、长按、触摸等操作。

3. UI Automator

​ UIAutomator 也是用来测试 Android 应用界面的主要方法,运行于测试机或者持续集成环境中的模拟器。不同于 Espresso, UIAutomator 还可以用来测试与系统界面的交互,如状态栏、通知、系统权限对话框等。

​ 这些测试编写完成后,都可以通过自动集成环境(如 Jenkins、Gitlab Runner 、Travis-ci、AppVeyor 等)自动编译、检查错误并生成报告。其中本地单元测试最容易集成但功能有限;仪表 (Instrumented) 单元测试、Espresso、 UI Automator 都需要在自动集成环境的无用户界面环境下启动模拟器并运行,可以附加配置自动抓日志、截屏等方式自动编译、检查错误并生成报告,集成配置相对会复杂一些。

三. 编写 Android 测试

注意: 强烈建议使用 Android Studio 编写测试应用,因为它已经集成了工程配置、依赖库和便利的包管理。本文档假定你已经在使用 Android Studio。

1. 本地单元测试

​ 本小节介绍如何编写本地单元测试,这类测试无依赖或者可以通过 mock 伪造依赖,运行于本地 JVM 。

​ 参考 Building Local Unit Tests

​ 当你编写的单元测试没有依赖或者对 Android 仅有一些简单依赖时,你就可以在本地开发环境运行测试。这个测试过程很高效,因为本地测试可以避免加载目标应用,同时也避免了将测试代码加载到开发机或者模拟器上,所以节省了测试执行过程花费的时间。通常,你需要使用一个 mocking 框架,例如 Mockito 来满足代码的依赖关系。

配置测试环境

​ 在你的 Android Studio 工程中,你必须将本地单元测试的代码存放在 module-name/src/test/java/ 目录下。这个目录在你新建工程时已经被默认创建。你同时也需要为你的工程配置测试依赖库,这个标准 API 库是由 JUnit 4 框架提供的。如果你的测试代码需要依赖 Android,则需要引入 Mockito 库来简化你的本地测试。阅读更多关于如何在你本地单元测试中使用 mock objects,请查看 Mocking Android dependencies

​ 在你 app 顶层的 build.gradble 文件中,你需要指定如下依赖库:

dependencies {
    // Required -- JUnit 4 framework
    testCompile 'junit:junit:4.12'
    // Optional -- Mockito framework
    testCompile 'org.mockito:mockito-core:1.10.19'
}

创建本地单元测试类

​ 本地单元测试类应该是一个纯粹的 JUnit 4 单元测试类。 JUnit 是一个广泛使用的 Java 单元测试框架。JUnit 4 是此框架的最新版本,与之前的几个版本相比,这个版本可以以更简洁和灵活的方式写测试。不同于之前的版本,你的测试类不再需要继承自 junit.framework.TestCase ,同时你也不再需要为测试方法增加 test 关键字前缀,或者使用任何 jnit.frameworkjnit.extensions 包。

​ 创建一个基本的 JUnit 4 测试类,你需要创建一个含有至少一个测试方法的 Java 类。一个测试方法需要以 @Test 注解开始,同时包含一个执行并验证组件中单一功能的代码块。

​ 例如下面的代码块,测试方法 emailValidator_CorrectEmailSimple_ReturnsTrue 验证 isValidEmail() 方法返回正确的值。

import org.junit.Test;
import java.util.regex.Pattern;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class EmailValidatorTest {

    @Test
    public void emailValidator_CorrectEmailSimple_ReturnsTrue() {
        assertThat(EmailValidator.isValidEmail("[email protected]"), is(true));
    }
    ...
}

使用 junit.Assert 一系列方法来验证检查组件执行的状态或结果是否符合预期,为了更好的可读性,你可以使用

Hamcrest matchers (如 is()equalTo() 方法)来匹配返回值和预期结果。

模拟 (mock) Android 依赖

​ Android Gradle 插件默认会使用一个修改版的 android.jar 库来执行你的本地单元测试,这个修改版的库不包含任何实现代码。与此同时,你本地的单元测试调用的 Android 类会抛出一个异常。这样做是为了确保你的单元测试代码不依赖于任何特定的 Android 平台行为(除非你明确 mock 这些类)。

​ 你可以使用 mocking 框架来模拟你代码中的外部依赖,来轻松的测试你的组件与依赖交互的结果符合预期。通过 Mock 对象替换 Android 依赖,你可以将单元测试和 Android 系统的其他部分分离,而只验证某些有简单依赖的代码块能够被正确执行。Java 的模拟框架 Mockito 从 1.9.5 版本开始兼容 Android 单元测试。使用 Mockito, 你可以配置 mock 对象在调用时返回一些特定的值。

​ 使用此框架为你本地的单元测试添加 mock 对象,请遵守如下编程模式:

​ 1. 在 build.gradle 中导入 Mockito 库依赖,如上文 设置你的测试环境 描述。

​ 2. 在你单元测试的类声明前添加 @RunWith(MockitoJUnitRunner.class) 注解。这个注解告诉 Mockito 测试执行者验证开发者是否正确的使用此框架,同时简化 mock 对象的初始化。

​ 3. 要为一个 Android 依赖如 Context 创建 mock 对象,请在变量声明前添加 @Mock 注解。

​ 4. 为了保持或者说模仿依赖行为,你可以使用 when()thenReturn() 方法来指定当满足某一特定条件时返回某个值。

​ 下面的代码示例展示了在单元测试中如何 mock 一个 Context 对象。

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.*;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import android.content.SharedPreferences;

@RunWith(MockitoJUnitRunner.class)
public class UnitTestSample {

    private static final String FAKE_STRING = "HELLO WORLD";

    @Mock
    Context mMockContext;

    @Test
    public void readStringFromContext_LocalizedString() {
        // Given a mocked Context injected into the object under test...
        when(mMockContext.getString(R.string.hello_word))
                .thenReturn(FAKE_STRING);
        ClassUnderTest myObjectUnderTest = new ClassUnderTest(mMockContext);

        // ...when the string is returned from the object under test...
        String result = myObjectUnderTest.getHelloWorldString();

        // ...then the result should be the expected one.
        assertThat(result, is(FAKE_STRING));
    }
}

​ 想要学习更多如何使用 Mockito 框架库的内容,请参考 Mockito API referencesample code 中的 SharedPreferencesHelperTest 类。

Error: "Method ... not mocked"

​ 如果你运行的测试调用了没有 mock 的 Android SDK API,你会收到一个 method is not mocked 的错误。这是因为单元测试执行过程中使用的 android.jar 文件没有包含任何实际的实现代码(这些 API 仅被提供在物理设备的系统镜像中)。

​ 相对的,所有的方法调用默认会抛出异常。这是为了确保你的单元测试中仅仅测试你的代码块,而你的代码不依赖于任何特定的 Android 平台行为(那些你没有明确模拟即 mock 的行为)。

​ 如果你认为抛出异常是个问题,你可以修改方法行为使它不抛出异常而返回空或者零,你需要在 build.gradle 文件中添加如下的代码段:

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

注意: 应该谨慎设置 returnDefaultValues 属性为 true 。返回空或零值可能会使你的测试变得不可靠,将可能很难调试或者可能会允许失败的测试通过。此方法应该仅作为不能解决问题时的最后手段。

运行本地单元测试

​ 使用如下步骤运行你本地的单元测试:

​ 1. 确保你的工程已经和 Gradle 同步,点击工具栏的 Sync Project img按钮。

​ 2. 使用如下几种途径之一运行你的测试:

  • 只运行一个测试,打开 Project 窗口, 在这个测试上点击右键再点击 Run img
  • 要测试某个类的所有测试,右键点击这个类或者类中的一个方法,再点击 Run img
  • 要测试某个目录中的所有测试,右键点击这个目录,再选择 Run tests img

​ Android gradle 插件会编译位于默认目录 (src/test/java/) 的本地单元测试,编译一个测试 app,然后使用默认的 test runner 类在本地执行它。之后 Android Studio 会在 Run 窗口显示执行结果。

2. 仪表 (Instrumented) 单元测试

​ 本小节介绍如何编写 Instrumented 单元测试,这类测试不能通过 mock 来伪造依赖,需要运行在开发机或者模拟器上。

​ 参考 Building Instrumented Unit Tests

​ Instrumented 测试或者称为仪表测试是运行在物理设备或者模拟器上,可以利用 Android 框架 API 及支持 API,如 Android 测试支持库。如果你的测试需要访问运行时仪表信息如 APP 的 Context,或者你的测试需要真实的 Android 框架组件实现如 Parcelable 或 SharedPreferences 对象,则你应该创建 Instrumented 测试。

​ 使用 Instrumented 单元测试可以帮助你减少编写和维护 mock 代码。当然你也可以自由的使用上一节的 mocking 框架来模拟任何依赖关系。

配置测试环境

​ 在你的 Android Studio 工程中,你必须将 Instrumented 测试代码保存在 module-name/src/androidTest/java/ 目录下。在你创建工程时这个目录已经被默认创建,同时包含了一个 Instrumented 测试示例代码。

​ 在你开始前,你应该先在 Android SDK 里下载 Android Testing Support Library,这个支持库提供了一些API 允许你快速的编写和运行 instrumented 测试代码。此测试支持库包含了 JUnit 4 test runner (AndroidJUnitRunner ) 和 UI 测试工具 (EspressoUI Automator) 的 API。

​ 和本地单元测试一样,为了访问测试支持库提供的 API,你也需要配置测试依赖。为了简化你的测试开发,你应该包含 Hamcrest 库,使用这个库的匹配器 API,你可以创建更灵活的断言。

​ 在顶层的 build.gradle 文件中,你需要指定如下依赖库:

dependencies {
    androidTestCompile 'com.android.support:support-annotations:24.0.0'
    androidTestCompile 'com.android.support.test:runner:0.5'
    androidTestCompile 'com.android.support.test:rules:0.5'
    // Optional -- Hamcrest library
    androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
    // Optional -- UI testing with Espresso
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
    // Optional -- UI testing with UI Automator
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
}

注意: 如果你的配置中引入了 support-annotations 编译依赖库和 espresso-core 测试编译依赖库,那么你可能会遇到依赖冲突的失败提示。为了修改这个问题,你需要更新 espresso-core 依赖如下:

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

​ 要使用 JUnit 4 测试类,请确保指定 AndroidJUnitRunner 为默认的仪表测试执行者,可以在 app 模块下的 build.gradle 文件中加入如下代码:

android {
    defaultConfig {
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

创建 Instrumented 单元测试类

​ 你的 Instrumented 单元测试类应该按照 JUnit 4 测试类格式编写。要了解更多创建 JUnit 4 测试类和使用 JUnit 4 断言和注解,请阅读上一节本地单元测试中创建本地单元测试类的内容。

​ 要创建一个 instrumented JUnit 4 测试类,你需要为该类开头添加一个 @RunWith(AndroidJUnit4.class) 的注解。如下示例代码展示了一个用来测试 LogHistory 类是否正确实现 Parcelable 接口的 instrumented 单元测试。

import android.os.Parcel;
import android.support.test.runner.AndroidJUnit4;
import android.util.Pair;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class LogHistoryAndroidUnitTest {

    public static final String TEST_STRING = "This is a string";
    public static final long TEST_LONG = 12345678L;
    private LogHistory mLogHistory;

    @Before
    public void createLogHistory() {
        mLogHistory = new LogHistory();
    }

    @Test
    public void logHistory_ParcelableWriteRead() {
        // Set up the Parcelable object to send and receive.
        mLogHistory.addEntry(TEST_STRING, TEST_LONG);

        // Write the data.
        Parcel parcel = Parcel.obtain();
        mLogHistory.writeToParcel(parcel, mLogHistory.describeContents());

        // After you're done with writing, you need to reset the parcel for reading.
        parcel.setDataPosition(0);

        // Read the data.
        LogHistory createdFromParcel = LogHistory.CREATOR.createFromParcel(parcel);
        List<Pair<String, Long>> createdFromParcelData = createdFromParcel.getData();

        // Verify that the received data is correct.
        assertThat(createdFromParcelData.size(), is(1));
        assertThat(createdFromParcelData.get(0).first, is(TEST_STRING));
        assertThat(createdFromParcelData.get(0).second, is(TEST_LONG));
    }
}

创建测试套件

​ 要组织 instrumented 单元测试,开发者可以将这些测试分组,之后将这些测试一起执行。测试套件可以被嵌套,测试套件可以和其他测试套件组合在一起来同时测试其他类组件。

​ 一个测试套件被包含在一个测试包中,类似于 main 应用包。按惯例,测试套件的包名通常会以 .suite 结尾,例如 com.example.android.testing.mysample.suite

​ 要为单元测试创建一个测试套件,请导入 JUnit RunWithSuite 类。在你的测试套件类中,添加 @RunWith(Suite.class)@Suite.SuitClasses() 注解。然后将独立的测试类或测试套件加入到 @Suite.SuiteClasses() 的参数中。下面示例代码展示了一个名称为 UnitTestSuite 的测试套件,它包含了 CalculatorInstrumentationTest and CalculatorAddParameterizedTest 两个单元测试。

import com.example.android.testing.mysample.CalculatorAddParameterizedTest;
import com.example.android.testing.mysample.CalculatorInstrumentationTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

// Runs all unit tests.
@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorInstrumentationTest.class,
        CalculatorAddParameterizedTest.class})
public class UnitTestSuite {}

运行 Instrumented 单元测试

​ 请按照如下步骤运行 Instrumented 单元测试:

​ 1. 确保你的工程和 Gradle 保持同步,点击工具栏的 Sync Project img 按钮。

​ 2. 按照以下步骤之一运行测试:

  • 只运行一个测试,打开 Project 窗口, 在这个测试上点击右键再点击 Run img
  • 要测试某个类的所有测试,右键点击这个类或者类中的一个方法,再点击 Run img
  • 要测试某个目录中的所有测试,右键点击这个目录,再选择 Run tests img

​ 位于默认位置 (src/androidTest/java/) 的 Instrumented 测试代码会被 Android Gradle 插件编译,生成一个测试 APK 和一个生产 APK 文件,同时安装两个 APK 文件到已连接的设备或模拟器,然后运行测试。之后 Android Studio 会在 Run 窗口展示 Instrumented 测试的执行结果。

3. 自动化用户界面测试

​ 本小节介绍如何编写运行于单 APP 和多个 APP ,用于验证用户交互处理是否正确的测试。

​ 参考 Automating User Interface Tests

​ 用户界面(UI) 测试可以确保应用满足功能要求,同时也验证应用具有较高的质量,由此保证应用能够成功被用户接受。

​ 一个简单的 UI 测试方法是让测试人员在目标应用上执行一系列预定的用户操作,然后验证应用行为是否符合预期。然而,这种人工方式有很多缺点如耗时、乏味、容易出错。一个更高效的方法是编写可以运行触发特定操作的自动化 UI 测试。自动化测试的方法有很多优点如快速、可靠、可重复。

​ 要自动化UI 测试你的应用,你需要将测试代码放置到 (src/androidTest/java) 目录。Android 的 Gradle 插件会基于你的测试代码编译一个测试 app,然后将它加载到目标设备上。你可以在你的测试代码中,使用 UI 测试框架模拟用户行为来操作目标应用,达到测试特定使用场景的目的。

​ 一般会创建两种 Android 应用自动化 UI 测试:

​ 1. 单应用 UI 测试:这种测试用来验证目标应用在用户执行某特定操作或输入后能得到期望的行为。例如验证点击了设置菜单可以跳转到设置界面。这种测试允许开发者检查目标应用是否能在用户交互后响应了正确的界面。 UI 测试框架如 Espresso 允许开发者通过编程模拟用户操作并测试复杂的应用内用户交互行为。

​ 2. 跨应用 UI 测试:这种测试用来验证多个应用之间的交互行为或应用于系统之间的交互行为。例如,开发者可以测试相机应用将图片分享给三方的社交媒体应用或分享给 Android 默认相册应用行为是否正确。支持跨应用的 UI 测试框架如 UI Automator 允许你创建此类场景的测试。

​ 本节后的内容将介绍如何使用 Android 测试支持库提供的工具和 API 来编写这些自动化测试。开始之前你应该已经按照上文配置测试环境的内容下载安装了测试支持库。

3.1. 单应用 UI 测试 - Espresso

​ 测试单应用的用户交互行为可以帮助开发者确保用户在与应用交互时不会遇到不期望的结果或者糟糕的用户体验。如果开发者需要验证应用的用户界面功能正常,则应该习惯于创建用户界面测试。

​ 由 Android 测试支持库提供的 Espresso 测试框架,提供了编写 UI 测试的 API 来模拟单应用用户交互行为。 Espresso 库可以运行在 Android 2.3.3 (API level 10) 和更高的 设备上。在测试应用时使用 Espresso 的一个关键的好处是它提供了自动化同步测试行为。Espresso 检测到应用的主线程空闲后,会寻找合适的时机运行你的测试命令,这样提高了开发者编写的测试的可靠性。这种特性同时可以将开发者从不得不增加如 Thread.sleep() 延时到测试代码的应急方案中解放出来。

配置 Espresso

​ 开始编写 Espresso UI 测试前,开发者需要确保正确放置测试代码到目录,并且已经添加了工程依赖,如上文中配置测试环境的内容。

​ 在你 app 模块的 build.gradle 文件中,你必须已经配置了 Espresso 库依赖:

dependencies {
    // Other dependencies ...
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
}

​ 关闭测试设备中的动画效果 -- 当测试设备中有系统动画时,可能会引起不可预料的结果而导致你的测试失败。在设备或模拟器中的 设置 - 开发者选项 中关闭所有的下列动画选项:

  • Window animation scale
  • Transition animation scale
  • Animator duration scale

​ 如果你想要配置你的工程使用所有的 Espresso 功能而不仅仅是核心提供的 API,请阅读这里的 资源

创建 Espresso 测试类

​ 请遵守如下编程模型来创建一个 Espresso 测试类:

​ 1. 通过 onView() 方法找到你想要测试的 Acvitivity UI 组件,或者onData() 方法找到AdapterView中的元素。 例如应用中的登录按钮。

​ 2. 通过 ViewInteraction.perform()DataInteraction.perform()方法模拟一个特定的用户交互操作例如点击登录按钮。要序列化的在该组件上执行多个操作,可以在方法中使用逗号分隔的列表参数。

​ 3. 如果有必要,重复上边的步骤,来模拟用户在多个 Activity 间操作目标应用。

​ 4. 使用 ViewAssertions 方法来检查上述交互操作执行后 UI 返回了期望的状态或行为。

​ 这几步在下文会有更加详细的描述。下面示例是一个基本的代码片段:

onView(withId(R.id.my_view))            // withId(R.id.my_view) is a ViewMatcher
        .perform(click())               // click() is a ViewAction
        .check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
结合 ActivityTestRule 使用 Espresso

下面部分描述了如何创建一个 JUnit 4 风格的 Espresso 测试,以及如何使用 ActivityTestRule 来减少重复的代码。通过使用 ActivityTestRule, 测试框架会为每个注解为 @Test 的方法运行要测试的 Activity。测试前,注解为 @Before 的所有方法都会被执行。在每个测试完成后,所有注解为 @After 的方法都会被执行,之后测试框架会关闭 Activity。

package com.example.android.testing.espresso.BasicSample;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
...

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {

    private String mStringToBetyped;

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
            MainActivity.class);

    @Before
    public void initValidString() {
        // Specify a valid string.
        mStringToBetyped = "Espresso";
    }

    @Test
    public void changeText_sameActivity() {
        // Type text and then press the button.
        onView(withId(R.id.editTextUserInput))
                .perform(typeText(mStringToBetyped), closeSoftKeyboard());
        onView(withId(R.id.changeTextBt)).perform(click());

        // Check that the text was changed.
        onView(withId(R.id.textToBeChanged))
                .check(matches(withText(mStringToBetyped)));
    }
}
访问 UI 组件

​ 在 Espresso 与应用交互前,开发者必须指定 UI 组件或 view, Espresso 支持使用 Hamcrest matchers 来匹配查找应用或者 adapter 中的 view 。

​ 要找到 view, 使用 onView() 方法并传入一个你的目标视图的 view matcher。在下一节 指定一个 view matcher 有更详细的介绍。 onView() 方法会返回一个 ViewInteraction 对象来允许开发者的测试和视图交互。然而,如果你想要获取一个 RecyclerView 中的视图,调用 onView() 方法可能会失效。在这种场景下,应该使用下文的在 AdapterView 中获取视图的方法。

注意: onView() 方法不会检查开发者指定的视图是否有效,Espresso 也仅在当前的布局层搜索匹配,如果找不到视图则会抛出一个 NoMatchingViewException 异常。

​ 下面的代码片段展示了开发者如何编写一个测试来访问一个 EditText 成员,输入一个字符串,关闭虚拟键盘,然后执行一个按钮点击。

public void testChangeText_sameActivity() {
    // Type text and then press the button.
    onView(withId(R.id.editTextUserInput))
            .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
    onView(withId(R.id.changeTextButton)).perform(click());

    // Check that the text was changed.
    ...
}
指定一个视图匹配器 View Matcher

​ 开发者可以使用如下方法来指定一个视图匹配器:

  • 使用 ViewMatchers 类中的方法,例如,要查找一个显示了某字符串的视图,可以使用如下方法:

    onView(withText("Sign-in"));
    

    类似的可以使用 withId() 方法并提供视图的资源 ID (R.id) 如下所示:

    onView(withId(R.id.button_signin));

    Android 的资源 ID 在设计上并不是唯一的。 如果开发者编写的测试尝试匹配一个被多个视图使用了的资源 ID Espresso 则会抛出 AmbiguousViewMatcherException 异常。

  • 使用 Hamcrest Matchers 类。可以使用 allOf() 方法来连接多个匹配器,如 containsString()instanceOf() 。这个方法允许开发者更精确的过滤匹配结果,如下代码所示:

    onView(allOf(withId(R.id.button_signin), withText("Sign-in")));

    可以使用 not 关键字来过滤不匹配的视图,如下代码所示:

    onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))));

    要在代码中使用这些方法, 请导入 org.hamcrest.Matchers 包。要学习更多关于 Hamcrest 匹配器的内容,请阅读 Hamcrest 网站

在 AdapterView 中查找视图

​ 在一个 AdapterView 部件中, 子视图是被运行时动态加载的。如果要测试的目标视图在 AdapterView (例如 ListView, GridView, 或 Spinner) 中, onView() 可能会失效,因为可能只有视图的一部分被加载在可见布局层。

​ 取而代之的,可以使用 onData() 方法来获取一个 DataInteraction 对象然后访问目标视图元素。 Espresso 会处理加载目标视图到当前的视图层。 Espresso 同样会处理目标视图元素的滚动操作, 同时将目标视图聚焦。

注意: onData() 方法不会检查开发者指定的数据对应于某个视图。 Espresso 只在当前视图层查找。 如果没有找到任何匹配,将会抛出 NoMatchingViewException 异常。

​ 下面的代码展示了如何使用 onData() 方法和 Hamcrest 来匹配查找包含给定字符串的行。 在这个例子中 LongListActivity 类包含了由 SimpleAdapter 暴露的字符串列表。

onData(allOf(is(instanceOf(Map.class)),
        hasEntry(equalTo(LongListActivity.ROW_TEXT), is("test input")));
模拟或执行用户操作

​ 使用 ViewInteraction.perform()DataInteraction.perform() 方法来模拟用户在 UI 组件上的交互行为。 开发者必须在参数中传递一个或多个 ViewAction 对象,Espresso 会按照给定的参数顺序在主线程执行每个操作。

ViewActions 类提供了一系列的帮助方法用来模拟常见行为。开发者可以使用这些方法来快捷方便的模拟行为而不是自己去创建配置独立的 ViewAction 对象。包括如下行为:

​ 如果目标视图在 ScrollView 内, 执行 ViewActions.scrollTo() 操作会先将目标视图展示到屏幕上,再执行其他操作。如果视图已经被显示在界面上。则 ViewActions.scrollTo() 操作不会执行任何动作。

使用 Espresso Intents 隔离测试 Activity

Espresso Intents 可以验证并保持应用发出的 Intent 。 使用Espresso Intents ,开发者可以拦截并保持发出的 Intent 和返回的结果,并发送他们到要测试的组件,从而达到隔离测试 app、activity、 或 service 的目的。

要使用 Espresso Intents 做测试, 需要添加如下内容到应用的 build.gradle 文件:

dependencies {
  // Other dependencies ...
  androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.2'
}

​ 要测试一个 Intent ,你需要创建一个类似于 ActivityTestRuleIntentsTestRule 类实例。IntentsTestRule 会在每个测试前初始化 Espresso Intents , 终止 activity , 并在每次测试完成后释放 Espresso Intents 。

如下测试类代码片段展示了如何测试 intent ,它测试了 Building Your First App 教程中创建的 activity 和intent。

@Large
@RunWith(AndroidJUnit4.class)
public class SimpleIntentTest {

    private static final String MESSAGE = "This is a test";
    private static final String PACKAGE_NAME = "com.example.myfirstapp";

    /* Instantiate an IntentsTestRule object. */
    @Rule
    public IntentsTestRule<MainActivity> mIntentsRule =
      new IntentsTestRule<>(MainActivity.class);

    @Test
    public void verifyMessageSentToMessageActivity() {

        // Types a message into a EditText element.
        onView(withId(R.id.edit_message))
                .perform(typeText(MESSAGE), closeSoftKeyboard());

        // Clicks a button to send the message to another
        // activity through an explicit intent.
        onView(withId(R.id.send_message)).perform(click());

        // Verifies that the DisplayMessageActivity received an intent
        // with the correct package name and message.
        intended(allOf(
                hasComponent(hasShortClassName(".DisplayMessageActivity")),
                toPackage(PACKAGE_NAME),
                hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE)));

    }
}

​ 更多关于 Espresso Intents 的内容, 请阅读 Espresso Intents documentation on the Android Testing Support Library 网站 。你也可以下载并阅读 IntentsBasicSampleIntentsAdvancedSample 代码示例。

使用 Espresso Web 测试 WebView

​ Espresso Web 可以用来测试 Activity 内的 WebView 组件。它使用 WebDriver API 来检查和控制 WebView 的行为。

​ 开始使用 Espresso Web 前,你需要添加如下依赖到 app 的 build.gradle 文件:

dependencies {
  // Other dependencies ...
  androidTestCompile 'com.android.support.test.espresso:espresso-web:2.2.2'
}

​ 使用 Espresso Web 创建测试, 实例化 ActivityTestRule 对象来测试 activity 时,开发者需要启用 WebView 上的 JavaScript 支持。在测试中, 开发者可以选择展示在 WebView 上的 HTML 元素,然后模拟用户交互行为。例如可以在一个 text box 中输入字符然后点击一个按钮。当操作执行完成后,开发者可以验证网页上的结果是不是符合预期。

​ 在下面的代码中,测试了一个 ID 为 'webview' 的 WebView 。 测试单元verifyValidInputYieldsSuccesfulSubmission() 选择了网页上的一个 <input> 元素,输入一些字符,然后检查出现在另一个元素上的字符串。

@LargeTest
@RunWith(AndroidJUnit4.class)
public class WebViewActivityTest {

    private static final String MACCHIATO = "Macchiato";
    private static final String DOPPIO = "Doppio";

    @Rule
    public ActivityTestRule mActivityRule =
        new ActivityTestRule(WebViewActivity.class,
            false /* Initial touch mode */, false /*  launch activity */) {

        @Override
        protected void afterActivityLaunched() {
            // Enable JavaScript.
            onWebView().forceJavascriptEnabled();
        }
    }

    @Test
    public void typeTextInInput_clickButton_SubmitsForm() {
       // Lazily launch the Activity with a custom start Intent per test
       mActivityRule.launchActivity(withWebFormIntent());

       // Selects the WebView in your layout.
       // If you have multiple WebViews you can also use a
       // matcher to select a given WebView, onWebView(withId(R.id.web_view)).
       onWebView()
           // Find the input element by ID
           .withElement(findElement(Locator.ID, "text_input"))
           // Clear previous input
           .perform(clearElement())
           // Enter text into the input element
           .perform(DriverAtoms.webKeys(MACCHIATO))
           // Find the submit button
           .withElement(findElement(Locator.ID, "submitBtn"))
           // Simulate a click via JavaScript
           .perform(webClick())
           // Find the response element by ID
           .withElement(findElement(Locator.ID, "response"))
           // Verify that the response page contains the entered text
           .check(webMatches(getText(), containsString(MACCHIATO)));
    }
}

​ 要学习更多关于 Espresso Web 的内容,请阅读 Espresso Web documentation on the Android Testing Support Library 网站。 你也可以下载并阅读 Espresso Web 示例代码

验证结果

​ 调用 ViewInteraction.check()DataInteraction.check() 方法来断言用户界面符合期望的状态。 开发者必须将 ViewAssertion 对象作为参数传入。 如果断言失败,Espresso 会抛出一个 AssertionFailedError 错误。

ViewAssertions 类提供了一些列一般场景断言的帮助方法。可以使用的断言包括:

​ 如下代码片段展示了如何验证上文中输入字符到 EditText 成员后显示的字符串:

public void testChangeText_sameActivity() {
    // Type text and then press the button.
    ...

    // Check that the text was changed.
    onView(withId(R.id.textToBeChanged))
            .check(matches(withText(STRING_TO_BE_TYPED)));
}
在设备或模拟器运行 Espresso 测试

​ 开发者可以从 Android Studio 或命令行运行 Espresso 测试。请确保已按照上文内容指定 AndroidJUnitRunner 作为工程默认的 instrumentation runner 。要运行 Espresso 测试,请参考上文 运行 Instrumented 单元测试

3.2. 跨应用 UI 测试 - UI Automator

​ 一个支持跨应用的 UI 测试可以让开发者验证应用在与其他应用或与系统交互时行为正常。例如,一个消息应用让用户输入一些文本,启动 Android 联系人选择器,然后用户可以选择消息的接收人,然后再返回到原应用控制,让用户提交信息。

​ 本小结将说明如何使用 Android 测试支持库提供的 UI Automator 测试框架来编写此类 UI 测试。UI Automator API 允许开发者与设备上的可见元素交互,而不管哪个 Activity 正持有焦点。测试可以通过方便的描述方法查找 UI 组件,如组件显示的文本或组件的内容描述。UI Automator 测试可以运行在 Android 4.3 (API 18) 及更高本版的设备上。

​ UI Automator 测试框架是一个基于 instrumentation 的 API 且运行在 AndroidJUnitRunner 测试执行者上。

配置 UI Automator

​ 在开始编写你的 UI 测试前,请先确保已经正确配置了工程依赖及测试代码存放路径,如上文开始测试内容。

在工程的 app 模块 build.gradle 文件中,你需要配置 UI Automator 的库依赖。

dependencies {
    ...
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
}

​ 编写 UI Automator 测试前,开发者需要先检查目标 app 的 UI 组件并确保它们可以被访问。下面两个小节将介绍这些优化的要点。

检查设备上的 UI

​ 在开始设计的测试前,开发者需要检查设备上的 UI 组件是可见的。为了确保 UI Automator 测试可以访问这些组件,请检查这些组件有可见的文字标签,android:contentDescription 值。

uiautomatorviewer 工具提供了一个便利的可视化界面检查工具来查看在设备前台显示的组件的属性。有了这些信息就可以创建更高细粒度的 UI Automator 测试。例如,开发者可以创建一个匹配特定可见属性的 UI 选择器。

​ 按照以下步骤运行 uiautomatorviewer 工具:

​ 1. 在物理设备上运行目标 app。

​ 2. 将设备与开发计算机连接。

​ 3. 打开终端窗口并进入 <android-sdk>/tools/ 目录。

​ 4. 运行命令 uiautomatorviewer

​ 要查看应用的 UI 属性,请按照如下步骤:

​ 1. 在 uiautomatorviewer 界面,点击 Device Screenshot 按钮。

​ 2. 在 uiautomatorviewer 工具里,移动鼠标到快照左侧面板来查看 UI 组件,组件属性将在右下面板显示,布局层次将在右上面板显示。

​ 3. 可选的,点击 Toggle NAF Nodes 按钮来查看不可访问的 UI 组件,这些组件可能只会显示组件的部分可用信息。

确保 Activity 可被访问

​ UI Automator 测试框架在实现了 Android 可访问性功能的应用上表现的更好。当开发者使用一个视图元素 View 或者一个继承了 View 的子类时,就不需要实现可访问性支持,因为 SDK 或支持库已经在这些类里实现了此功能。

​ 有些应用会使用自定义个 UI 元素来提供一个更丰富的用户体验。这些元素不会提供自动的可访问性支持。如果开发者的应用包含了非 SDK 或支持库 View 子类的实例,请确保已经按照如下步骤实现了可访问性功能:

​ 1. 创建一个继承自 ExploreByTouchHelper 的实现类。

​ 2. 通过调用 [setAccessibilityDelegate()](https://developer.android.com/reference/android/support/v4/view/ViewCompat.html#setAccessibilityDelegate(android.view.View, android.support.v4.view.AccessibilityDelegateCompat)) 方法将自定义的 UI 元素实例关联。

​ 要学习更多关于自定义视图增加可访问性的功能,请阅读 Building Accessible Custom Views 。要学习更多关于 Android 可访问性的最佳实践,请阅读 Making Apps More Accessible

创建 UI Automator 测试

​ UI Automator 测试类的编写应该和 JUnit 4 测试类一致。要学习更多关于创建 JUnit 4 测试类和使用 Junit4 断言及注解,请阅读上文的 创建 Instrumented 单元测试类。

​ 在类的声明部分前增加 @RunWith(AndroidJUnit4.class) 注解。开发者同样需要指定由 Android 测试支持库提供的 AndroidJUnitRunner 类为默认的测试运行者。

​ 请遵循如下变成模型来编写 UI Automator 测试类:

​ 1. 通过 getInstance() 方法获取测试设备的 UIDevice 对象,同时传入一个 Instrumentation 对象参数。

​ 2. 通过 findObject() 方法获取一个 UiObject 对象来访问设备上显示的 UI 组件。

​ 3. 通过 UiObject 的方法如 performMultiPointerGesture() (模拟多点手势) 或 setText() (编辑文本) 模拟一个特定的用户交互行为。如果有必要,可以重复使用 API 重复创建步骤 2 和步骤 3 中的操作来实现更复杂的用户交互行为。

​ 4. 执行用户交互行为后,检查 UI 反映结果是否符合预期的状态或行为。

​ 这些步骤将在下面几小节详细描述。

访问 UI 组件

UiDevice 对象是开发者访问和操作设备状态的主要方式。在测试中,开发者可以调用 UiDevice 方法来检查不同的属性状态,例如当前的屏幕方向或显示大小。测试可以使用 UiDevice 对象执行设备级别的操作,如强制设备屏幕转向,按下 D-pad 硬件按钮,按下 Home 或 菜单按钮。

​ 从设备的主屏幕开始测试是个很好的做法。从主屏幕开始,开发者可以调用 UI Automator API 提供的方法来选择特定的 UI 组件并交互。如下代码片段展示了如何获取 UiDevice 实例并模拟按下 Home 按钮。

import org.junit.Before;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.Until;
...

@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 18)
public class ChangeTextBehaviorTest {

    private static final String BASIC_SAMPLE_PACKAGE
            = "com.example.android.testing.uiautomator.BasicSample";
    private static final int LAUNCH_TIMEOUT = 5000;
    private static final String STRING_TO_BE_TYPED = "UiAutomator";
    private UiDevice mDevice;

    @Before
    public void startMainActivityFromHomeScreen() {
        // Initialize UiDevice instance
        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

        // Start from the home screen
        mDevice.pressHome();

        // Wait for launcher
        final String launcherPackage = mDevice.getLauncherPackageName();
        assertThat(launcherPackage, notNullValue());
        mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)),
                LAUNCH_TIMEOUT);

        // Launch the app
        Context context = InstrumentationRegistry.getContext();
        final Intent intent = context.getPackageManager()
                .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
        // Clear out any previous instances
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
        context.startActivity(intent);

        // Wait for the app to appear
        mDevice.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)),
                LAUNCH_TIMEOUT);
    }
}

​ 在示例代码中, @SdkSuppress(minSdkVersion = 18) 声明确保了测试只会运行在 Android 4.3 (API 18) 或更高的版本上,正如 UI Automator 框架所要求的那样。

​ 使用 findObject() 方法获取一个展现在屏幕上,匹配了给定选择器的 UiObject 对象。如果需要,可以在代码中重复利用已经创建的 UiObject 实例。注意 UI Automator 测试框架会在每次使用 UiObject 点击或查询属性时在当前界面重新查找匹配。

​ 如下代码展示了如何使用 UiObject 实例来操作应用中的 Cancel 按钮和 OK 按钮。

UiObject cancelButton = mDevice.findObject(new UiSelector()
        .text("Cancel"))
        .className("android.widget.Button"));
UiObject okButton = mDevice.findObject(new UiSelector()
        .text("OK"))
        .className("android.widget.Button"));

// Simulate a user-click on the OK button, if found.
if(okButton.exists() && okButton.isEnabled()) {
    okButton.click();
}

指定选择器

​ 请使用 UiSelector 类来访问特定的 UI 组件。如果找到了多个视图元素,布局层次中的第一个匹配元素将被返回为 UiObject 。构造 UiSelector 时,可以将多个属性串联来提炼查找。如果没有找到匹配的元素,将抛出 UiAutomatorObjectNotFoundException 异常。

​ 可以使用 childSelector() 方法来嵌套多个 UiSelector 实例。例如下面代码展示了一个测试,在当前显示的视图中,找到第一个 ListView ,然后在这个 ListView 中查找匹配文本属性 Apps 的视图元素。

UiObject appItem = new UiObject(new UiSelector()
        .className("android.widget.ListView")
        .instance(0)
        .childSelector(new UiSelector()
        .text("Apps")));

​ 作为最佳实践,开发者应该使用资源 ID 来指定一个选择器,而不是使用文本值或者内容描述。不是所有的视图元素都含有文本内容。文本选择器很脆弱,可能会在 UI 发生很小变化时导致测试失败。同样,文本选择器可能在多语言环境下失效,它们可能匹配不到翻译后的字符串。

​ 在选择器中指定对象状态可能会很有用。例如可以通过 checked() 方法传入参数 true 选择列表中所有的已选择元素,然后可以取消这些元素的选择。

执行用户操作

​ 当获取到 UiObject 对象后,开发者可以通过调用 UiObject 类中的方法来执行 UI 组件的用户交互行为。可以执行的操作包括:

​ UI Automator 测试框架允许开发者通过 getContext() 方法获得一个 Context 对象来发送 Intent 或启动 Activity , 不依赖 shell 命令。

​ 下面代码片段展示了测试中如何发送一个Intent 来启动应用。当只对测试计算器应用感兴趣时,此方法非常有用,这个途径并不关心谁是启动器。

public void setUp() {
    ...

    // Launch a simple calculator app
    Context context = getInstrumentation().getContext();
    Intent intent = context.getPackageManager()
            .getLaunchIntentForPackage(CALC_PACKAGE);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
            // Clear out any previous instances
    context.startActivity(intent);
    mDevice.wait(Until.hasObject(By.pkg(CALC_PACKAGE).depth(0)), TIMEOUT);
}

在集合上执行操作

​ 如果要模拟一个集合上(如音乐专辑里的歌曲或收件箱里的邮件)的用户操作,可以使用 UiCollection 类。 要创建一个 UiCollection 对象,指定一个 UiSelector 来查找一个 UI 包装器或其他 UI 元素的包装器,如一个包含子 UI 元素的布局视图。

​ 如下代码展示了如何构造一个 UiCollection

UiCollection videos = new UiCollection(new UiSelector()
        .className("android.widget.FrameLayout"));

// Retrieve the number of videos in this collection:
int count = videos.getChildCount(new UiSelector()
        .className("android.widget.LinearLayout"));

// Find a specific video and simulate a user-click on it
UiObject video = videos.getChildByText(new UiSelector()
        .className("android.widget.LinearLayout"), "Cute Baby Laughing");
video.click();

// Simulate selecting a checkbox that is associated with the video
UiObject checkBox = video.getChild(new UiSelector()
        .className("android.widget.Checkbox"));
if(!checkBox.isSelected()) checkbox.click();
在可滚动视图上执行操作

​ 使用 UiScrollable 模拟垂直或水平滚动显示。UI 元素位于屏幕外时,需要滚动以将其置于视图中。如下的代码片段展示了模拟向下滚动设置菜单,然后点击一个 "About tablet" 选项:

UiScrollable settingsItem = new UiScrollable(new UiSelector()
        .className("android.widget.ListView"));
UiObject about = settingsItem.getChildByText(new UiSelector()
        .className("android.widget.LinearLayout"), "About tablet");
about.click();
验证结果

InstrumentationTestCase 类继承自 TestCase,所以可以使用 JUnit Assert 方法来测试 UI 组件返回了期望的结果。下面的代码片段展示了如何定位计算器应用中的多个按钮,然后按顺序点击,最后确认显示了正确的结果:

private static final String CALC_PACKAGE = "com.myexample.calc";

public void testTwoPlusThreeEqualsFive() {
    // Enter an equation: 2 + 3 = ?
    mDevice.findObject(new UiSelector()
            .packageName(CALC_PACKAGE).resourceId("two")).click();
    mDevice.findObject(new UiSelector()
            .packageName(CALC_PACKAGE).resourceId("plus")).click();
    mDevice.findObject(new UiSelector()
            .packageName(CALC_PACKAGE).resourceId("three")).click();
    mDevice.findObject(new UiSelector()
            .packageName(CALC_PACKAGE).resourceId("equals")).click();

    // Verify the result = 5
    UiObject result = mDevice.findObject(By.res(CALC_PACKAGE, "result"));
    assertEquals("5", result.getText());
}
在设备或模拟器运行 UI Automator 测试

​ 开发者可以从 Android Studio 或命令行运行 UI Automator 测试。请确保已指定 AndroidJUnitRunner 作为工程默认的 instrumentation runner 。要运行 UI Automator 测试,请参考上文 运行 Instrumented 单元测试。

4. 应用组件集成测试

​ 本小节介绍如何编写用户不直接参与交互的组件测试,如 Service 和 Content provider。

​ 参考 Testing App Compontent Integrations

​ 如果应用使用了用户不会直接参与交互的组件,例如 Service 和 Content Provider,则应当验证应用内的这些组件行文正确。

​ 当开发这些组件时,编写验证这些组件功能是否正确的集成测试是个好习惯。

注意: Android 没有提供独立 BroadcastReceiver 的测试类。要验证 BroadcastReceiver 响应,需要测试发送出 Intent 对象的组件。另外,可以通过 InstrumentationRegistry.getTargetContext() 方法创建一个 BroadcastReceiver 的实例,然后测试 BroadcastReceiver 的方法 ( 通常是 onReceive() 方法 ) 。

​ 这一节将介绍如何使用 Android 平台提供的 API 和工具编写自动化集成测试。

4.1. 测试 Service

​ 如果实现了一个本地 Service 作为应用的一个组件,那么开发者应当通过测试确保它按照预期的行为运行。可以创建 instrumented 单元测试 来验证 Service 中的行为正确。例如,服务储存并返回了有效的数据同时数据操作正确。

​ Android 测试支持库提供了隔离测试 Service 对象的 API, ServiceTestRule 类是一个用来在测试运行前启动Service 并在测试结束后完成 Service 的 JUnit 4 规则。通过这个测试规则,可以确保在每次测试运行前,已建立到服务的连接。

注意: ServiceTestRule 类不提供对 IntentService 对象测试的支持。如果需要测试一个 IntentService 对象,请将它的逻辑实现分离到另一个独立的类里,然后创建类的单元测试。

配置测试环境

​ 请参考上文的配置测试环境内容。

创建一个 Service 的集成测试

​ Service 的集成测试应该被写作 JUnit 4 测试类的格式。要创建一个 Service 的测试,请在测试类的声明前增加 @RunWith(AndroidJUnit4.class) 注解。同时,如上文内容所述,需要指定 AndroidJUnitRunner 类为默认的测试运行者。

​ 然后通过 @Rule 注解,创建一个 ServiceTestRule 实例:

@Rule
public final ServiceTestRule mServiceRule = new ServiceTestRule();

​ 如下代码片段展示了如何实现一个 service 的集成测试。测试方法 testWithBoundService 验证了应用是否成功绑定到一个本地服务,同时验证了服务接口行为是否正确。

@Test
public void testWithBoundService() throws TimeoutException {
    // Create the service Intent.
    Intent serviceIntent =
            new Intent(InstrumentationRegistry.getTargetContext(),
                LocalService.class);

    // Data can be passed to the service via the Intent.
    serviceIntent.putExtra(LocalService.SEED_KEY, 42L);

    // Bind the service and grab a reference to the binder.
    IBinder binder = mServiceRule.bindService(serviceIntent);

    // Get the reference to the service, or you can call
    // public methods on the binder directly.
    LocalService service =
            ((LocalService.LocalBinder) binder).getService();

    // Verify that the service is working correctly.
    assertThat(service.getRandomInt(), is(any(Integer.class)));
}
运行 Service 集成测试

​ 开发者可以从 Android Studio 或命令行运行 Service 集成测试。请确保已指定 AndroidJUnitRunner 作为工程默认的 instrumentation runner 。要运行 Service 集成测试,请参考上文 运行 Instrumented 单元测试。

4.2. 测试 Content Provider

​ 如果实现了一个 content provider 来保存或获取数据,或向其他应用提供可访问的数据,开发者应当编写测试来确保 content provider 的行为正确无误。这一小节将介绍如何编写 content provider 测试。

创建 Content Provider 集成测试

​ 在 Android 中,应用把 content provider 看作提供数据表的数据 API ,而看不到它们的内部实现。一个内容提供者可能有很多公共常量,但是很少有 public 方法或 public 变量。因为这个原因,需要编写只基于 provider 公开成员的测试。

​ Content Provider 允许访问实际的用户数据,因此对 content provider 的测试应该在隔离环境,这同样意味着测试不应该修改实际的用户数据。例如,应该避免由于之前的测试遗留数据导致当前测试失败。同样的,测试应该避免增加或删除 provider 的实际数据。

​ 为了将 content provider 隔离,需要使用 ProviderTestCase2 类。这个类允许开发者使用 Android mock 对象类,如 IsolatedContextMockContentResolver 来访问文件和数据库信息而不影响真实用户数据。

​ Content Provider 的集成测试应该编写为 JUnit 4 测试类,更多关于创建 JUnit 4 测试类和使用 JUnit 4 断言,请阅读上文中创建本地单元测试类的内容。

​ 要创建 content provider 的集成测试,请按照如下几个步骤:

  • 创建一个继承自 ProviderTestCase2 的子类。

  • 类声明部分前添加 @RunWith(AndroidJUnit4.class) 注解。

  • 指定 AndroidJUnitRunner 类为默认的测试执行者。

  • InstrumentationRegistry 设置 Context 对象,如下所示:

    @Override
    protected void setUp() throws Exception {
        setContext(InstrumentationRegistry.getTargetContext());
        super.setUp();
    }
ProviderTestCase2 是如何工作的

​ 通过 ProviderTestCase2 的子类测试 provider,这个类继承自 AndroidTestCase,所以它提供了 JUnit 测试框架和用来测试应用的 Android 特定方法。这个类的最重要的一个功能是在它初始化时创建了一个独立的测试环境。

​ 初始化过程是在 ProviderTestCase2 的构造器中完成的,ProviderTestCase2 构造器创建了一个 IsolatedContext 对象,以允许文件和数据库操作与 Android 系统隔离。文件和数据库操作会被放在设备或模拟器的一个有特殊前缀的独立目录中。

​ 之后构造器会创建一个 MockContentResolver 来作为测试的 resolver。最终,构造器创建一个 provider 实例用来测试。这是一个普通的 ContentProvider 对象,同时它从 IsolatedContext 中获取所有的环境信息。由此 provider 被限制在独立的测试环境中。

​ Content Provider 的集成测试和 instrumented 单元测试一致,要运行 integration 测试,请参考上文运行 integration 测试的内容。

测试什么

​ 如下是几条测试 content provider 的具体指导:

  • 使用 Resolver 方法测试: 即使开发者可以在 ProviderTestCase2 中实例化一个 provider 对象,也应该使用一个 resolver 对象及适当的 URI 测试 provider。使用 resolver 可以确保开发者像常规应用交互那样测试 provider 。
  • 像合约一样测试一个公开的 provider,如果开发者期望 provider 公开的被其他应用访问,则需要将它作为一个合约测试,一些如何做的例子如下:
    • 测试 provider 暴露的常量。例如, 检查 provider 数据表的栏目名常量,这些数据库字段名总是公开的常量。
    • 测试所有 provider 提供的 URI。provider 可能提供多个 URI,每个 URI 代表着不同的数据。
    • 测试无效 URI :开发者的单元测试应该故意调用一个无效的 URI,来检查是否报错。 一个好的 Provider 设计会为无效 URI 抛出 IllegalArgumentException 异常。
  • 测试标准的 Provider 交互: 大多数 Provider 提供六个方法: query(), insert(), delete(), update(), getType(), and onCreate()。 开发者的测试应该验证所有的这些方法可以工作。 这些方法在 Content Providers 中有更多描述。
  • 测试业务逻辑: 如果 content provider 实现了业务逻辑,则开发者需要测试它。

5. 测试 UI 性能

​ UI 性能测试不需要开发人员编写代码,请参考 Testing Display Performance

四. 代码覆盖率

​ 代码覆盖率即 Code Coverage 是编写的测试代码完整度的衡量标准。一个完整的工程应该有代码覆盖率报告。

​ 请参考 https://github.com/codecov/example-android

五. 参考资料

Testing Apps on Android

Testing UI for Multiple Apps

Android Architecture Blueprints

MainActivityTest.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment