测试基础知识

用户可以在各个层面与您的应用进行交互,从按某个按钮到将信息下载到他们的设备上。因此,在迭代开发您的应用时,您应测试各种用例和交互。

组织整理代码以便测试

随着应用的扩展,您可能会发现有必要从服务器获取数据、与设备的传感器进行交互、访问本地存储或呈现复杂的界面。应用的多功能性需要全面的测试策略。

迭代创建和测试代码

迭代开发某项功能时,您可以先编写一个新测试,也可以将用例和断言添加到现有单元测试。测试最初会失败,因为该功能尚未实现。

务必考虑随着设计新功能而出现的责任单元。对于每个单元,您需要编写相应的单元测试。您的单元测试应几乎囊括与单元的所有可能的交互,包括标准交互、无效输入以及资源不可用的情况。应尽可能利用 Jetpack 库;当您使用这些经过充分测试的库时,您可以专注于验证您的应用特有的行为。

在测试开发周期中,先编写失败的单元测试,再编写代码以使其通过测试,然后重构。整个功能开发周期存在于一个基于界面的更大周期的一个步骤中。
图 1. 与由测试驱动的迭代开发关联的两个周期

完整的工作流(如图 1 所示)包含一系列嵌套的迭代周期,其中一个由界面驱动的漫长而缓慢的周期用来测试代码单元的集成。您可以使用更短且更快的开发周期来测试单元本身。这一组周期一直持续到您的应用满足每个用例为止。

将应用看作一系列模块

为了使您的代码更易于测试,应从模块的角度进行开发,其中每个模块代表用户在您的应用中完成的一项特定任务。这种开发角度与基于堆栈的应用视图(通常包含代表界面、业务逻辑和数据的层)形成对比。

例如,“任务列表”应用可能包含用于创建任务的模块、查看有关已完成任务的统计信息的模块,以及拍摄要与特定任务相关联的照片的模块。这种模块化架构还可以帮助您使不相关的类保持分离,并为在开发团队内分配所有权提供了一个自然的结构。

务必在每个模块周围设置明确定义的界限,并随着应用规模和复杂性的增长而创建新模块。每个模块应只重点关注一个领域,并且用于模块间通信的 API 应保持一致。为更轻松、快捷地测试这些模块间的交互,不妨考虑创建模块的虚假实现。在测试中,一个模块的真实实现可以调用另一个模块的虚假实现。

不过,当您创建新模块时,不要过于武断地马上使其功能齐全。特定模块可以没有应用堆栈的一层或多层。

要详细了解如何在应用中定义模块,以及创建和发布模块的平台支持,请参阅 Android App Bundle

配置测试环境

设置在应用中创建测试所需的环境和依赖项时,请遵循本部分中所述的最佳做法。

根据执行环境组织整理测试目录

Android Studio 中的典型项目包含两个用于放置测试的目录。请按以下方式组织整理您的测试:

  • androidTest 目录应包含在真实或虚拟设备上运行的测试。此类测试包括集成测试、端到端测试,以及仅靠 JVM 无法完成应用功能验证的其他测试。
  • test 目录应包含在本地计算机上运行的测试,如单元测试。

考虑在不同类型的设备上运行测试的利弊

在设备上运行测试时,您可以从以下类型中进行选择:

  • 真实设备
  • 虚拟设备(如 Android Studio 中的模拟器
  • 模拟设备(如 Robolectric)

真实设备可提供最高的保真度,但运行测试所花费的时间也最多。另一方面,模拟设备可提供较高的测试速度,但代价是保真度较低。不过,平台在二进制资源和逼真的循环程序上的改进使得模拟设备能够产生更逼真的结果。

虚拟设备则平衡了保真度和速度。当您使用虚拟设备进行测试时,可以使用快照来最大限度地缩短测试之间的设置时间。

考虑是否要使用测试替身

创建测试时,您可以选择创建真实对象或测试替身,如虚假对象或模拟对象。通常,在测试中使用真实对象比使用测试替身要好,尤其是当被测对象满足以下某个条件时:

  • 该对象是数据对象。
  • 除非与依赖项的真实对象版本通信,否则该对象无法运行。事件回调处理程序就是一个很好的例子。
  • 很难复制该对象与依赖项的通信。SQL 数据库处理程序就是一个很好的例子,其中内存中数据库提供的测试比数据库结果的虚假对象更可靠。

特别是,模拟您并不拥有的类型的实例通常会导致测试很脆弱,只有在您已经了解其他人实现该类型的复杂性时,测试才有效。只在万不得已时才使用此类模拟。您可以模拟自己的对象,但请注意,使用 @Spy 标注的模拟比对类中的所有功能打桩的模拟提供的保真度要高。

不过,如果您的测试尝试对真实对象执行以下类型的操作,则最好创建虚假对象甚至是模拟对象:

  • 长时间的操作,如处理大文件。
  • 非封闭型操作,如连接到任意开放端口。
  • 难以创建的配置。

提示:请与库创建者联系,了解他们是否提供了任何官方支持的测试基础架构(如虚假对象)能让您可靠地依赖。

编写测试

配置完测试环境后,就该编写用来评估应用功能的测试了。本部分介绍如何编写小型、中型和大型测试。

测试金字塔的级别

包含三层的金字塔
图 2. 测试金字塔,显示了应用的测试套件应包含的三类测试

测试金字塔(如图 2 所示)说明了应用应如何包含三类测试(即小型、中型和大型测试):

  • 小型测试是指单元测试,用于验证应用的行为,一次验证一个类。
  • 中型测试是指集成测试,用于验证模块内堆栈级别之间的交互或相关模块之间的交互。
  • 大型测试是指端到端测试,用于验证跨越了应用的多个模块的用户操作流程。

沿着金字塔逐级向上,从小型测试到大型测试,各类测试的保真度逐级提高,但维护和调试工作所需的执行时间和工作量也逐级增加。因此,您编写的单元测试应多于集成测试,集成测试应多于端到端测试。虽然各类测试的比例可能会因应用的用例不同而异,但我们通常建议各类测试所占比例如下:小型测试占 70%,中型测试占 20%,大型测试占 10%

要详细了解 Android 测试金字塔,请观看 2017 年 Google I/O 大会的 Android 平台上的测试驱动型开发会议视频(从 1 分 51 秒开始)。

编写小型测试

您编写的小型测试应该是高度集中的单元测试,能够详尽地验证应用中每个类的功能和约定。

在特定类中添加和更改方法时,请针对它们创建和运行单元测试。如果这些测试依赖于 Android 框架,请使用与设备无关的统一 API,如 androidx.test API。这种一致性可让您在本地运行测试,而无需使用物理设备或模拟器。

如果您的测试依赖于资源,请在应用的 build.gradle 文件中启用 includeAndroidResources 选项。然后,您的单元测试可以访问编译版本的资源,从而使测试更快速且更准确地运行。

app/build.gradle

    android {
        // ...

        testOptions {
            unitTests {
                includeAndroidResources = true
            }
        }
    }
    

本地单元测试

尽可能使用 AndroidX Test API,以便您的单元测试可以在设备或模拟器上运行。对于始终在由 JVM 驱动的开发计算机上运行的测试,您可以使用 Robolectric

Robolectric 会模拟 Android 4.1(API 级别 16)或更高版本的运行时环境,并提供由社区维护的虚假对象(称为“影子”)。通过此功能,您可以测试依赖于框架的代码,而无需使用模拟器或模拟对象。Robolectric 支持 Android 平台的以下几个方面:

  • 组件生命周期
  • 事件循环
  • 所有资源

插桩单元测试

您可以在物理设备或模拟器上运行插桩单元测试。不过,这种形式的测试所用的执行时间明显多于本地单元测试,因此,最好只有在必须根据实际设备硬件评估应用的行为时才依靠此方法。

运行插桩测试时,AndroidX Test 会使用以下线程:

  • 主线程,也称为“界面线程”或“Activity 线程”,界面交互和 Activity 生命周期事件发生在此线程上。
  • 插桩线程,大多数测试都在此线程上运行。当您的测试套件开始时,AndroidJUnitTest 类将启动此线程。

如果您需要在主线程上执行某个测试,请使用 @UiThreadTest 标注该测试。

编写中型测试

除了通过运行小型测试来测试应用的每个单元之外,您还应从模块级别验证应用的行为。为此,请编写中型测试,即用于验证一组单元的协作和交互的集成测试。

您可以根据应用的结构和以下中型测试示例(按范围递增的顺序)来定义表示应用中的单元组的最佳方式:

  1. 视图和视图模型之间的交互,如测试 Fragment 对象、验证布局 XML 或评估 ViewModel 对象的数据绑定逻辑。
  2. 应用的代码库层中的测试,验证不同数据源和数据访问对象 (DAO) 是否按预期进行交互。
  3. 应用的垂直切片,测试特定屏幕上的交互。此类测试目的在于验证应用堆栈的所有各层的交互。
  4. 多 Fragment 测试,评估应用的特定区域。与本列表中提到的其他类型的中型测试不同,这种类型的测试通常需要真实设备,因为被测交互涉及多个界面元素。

要执行这些测试,请执行以下操作:

  1. 使用 Espresso-Intents 库中的方法。要简化传入这些测试的信息,请使用虚假对象和打桩。
  2. 结合使用 IntentSubject 和基于 Truth 的断言来验证捕获的 intent。

运行插桩中型测试时使用 Espresso

当您在设备或 Robolectric 上执行类似于下面的界面交互时,Espresso 有助于使任务保持同步:

  • View 对象执行操作。
  • 评估具有无障碍功能需求的用户如何使用您的应用。
  • 找到并激活 RecyclerViewAdapterView 对象中的项。
  • 验证传出 intent 的状态。
  • 验证 WebView 对象中 DOM 的结构。

要详细了解这些交互以及如何在应用的测试中使用它们,请参阅 Espresso 指南。

编写大型测试

尽管单独测试应用中的每个类和模块很重要,但验证可引导用户使用多个模块和功能的端到端工作流也同样重要。这些类型的测试会在您的代码中形成不可避免的瓶颈,但您可以通过验证尽可能接近实际成品的应用来最大限度地减轻这种影响。

如果您的应用足够小,您可能只需要一套大型测试来评估应用的整体功能。否则,您应按团队所有权、功能垂直领域或用户目标来划分大型测试套件。

通常,最好在模拟设备或基于云的服务(如 Firebase 测试实验室)上而不是在物理设备上测试您的应用,因为这样您可以更方便快捷地测试屏幕尺寸和硬件配置的多种组合。

Espresso 中的同步支持

除了支持中型插桩测试之外,Espresso 还支持在大型测试中完成以下任务时实现同步:

  • 完成跨应用的进程界限的工作流。 仅适用于 Android 8.0(API 级别 26)及更高版本。
  • 跟踪应用中长时间运行的后台操作。
  • 执行设备外测试。

要详细了解这些交互以及如何在应用的测试中使用它们,请参阅 Espresso 指南。

使用 AndroidX Test 完成其他测试任务

本部分介绍如何使用 AndroidX Test 的元素来进一步优化应用的测试。

使用 Truth 创建更容易读懂的断言

Guava 团队提供了一个名为 Truth 的流利断言库。在构建测试的验证步骤(或 then 步骤)时,您可以使用此库来代替基于 JUnit 或 Hamcrest 的断言。

通常,您可以使用 Truth 来表达某个对象具有特定属性,使用的短语包含您要测试的条件,例如:

  • assertThat(object).hasFlags(FLAGS)
  • assertThat(object).doesNotHaveFlags(FLAGS)
  • assertThat(intent).hasData(URI)
  • assertThat(extras).string(string_key).equals(EXPECTED)

AndroidX Test 支持 Android 的其他几个主题,以使基于 Truth 的断言更易于构建:

AndroidX Test API 可帮助您执行与移动应用测试相关的常见任务,下面几部分将对此进行介绍。

编写界面测试

Espresso 可让您以编程方式且以线程安全的方式找到应用中的界面元素并与之交互。要了解详情,请参阅 Espresso 指南。

运行界面测试

AndroidJUnitRunner 类定义了一个基于插桩的 JUnit 测试运行程序,可让您在 Android 设备上运行 JUnit 3 或 JUnit 4 型测试类。该测试运行程序可帮助您将测试软件包和被测应用加载到设备或模拟器上、运行测试并报告结果。

要进一步提高这些测试的可靠性,请使用 Android Test Orchestrator,它在自己的 Instrumentation 沙盒中运行每个界面测试。这种架构减少了测试之间的共享状态,并在每个测试的基础上隔离了应用崩溃。如需详细了解 Android Test Orchestrator 在测试应用时提供的优势,请参阅 Android Test Orchestrator 指南。

与可见元素进行交互

通过 UI Automator API,您可以与设备上的可见元素进行交互,而不管获得焦点的是哪个 Activity 或 Fragment。

注意:我们建议只有在您的应用必须与系统界面或其他应用进行交互以完成关键用例时,才使用 UI Automator 测试您的应用。由于 UI Automator 与特定的系统界面进行交互,因此您必须在每次平台版本升级后以及在每次发布新版本的 Google Play 服务后重新运行并修复 UI Automator 测试。

作为使用 UI Automator 的替代方法,我们建议您添加封闭测试或将大型测试分成一套中小型测试。特别是,一次重点测试一段应用间通信,如向其他应用发送信息和响应 intent 结果。Espresso-Intents 工具可以帮助您编写这些较小的测试。

添加无障碍功能检查以验证普遍易用性

应用的界面应允许所有用户(包括具有无障碍功能需求的用户)与设备进行交互,并在应用中更轻松地完成任务。

为了帮助验证应用的无障碍功能,Android 测试库提供了几项内置功能,下面几部分将对此进行介绍。要详细了解如何验证应用对不同类型用户的易用性,请参阅关于测试应用的无障碍功能的指南。

Robolectric

您可以通过在测试套件开头添加 @AccessibilityChecks 注释来启用无障碍功能检查,如以下代码段所示:

Kotlin

    import org.robolectric.annotation.AccessibilityChecks

    @AccessibilityChecks
    class MyTestSuite {
        // Your tests here.
    }
    

Java

    import org.robolectric.annotation.AccessibilityChecks;

    @AccessibilityChecks
    public class MyTestSuite {
        // Your tests here.
    }
    

Espresso

您可以通过在测试套件的 setUp() 方法中调用 AccessibilityChecks.enable() 来启用无障碍功能检查,如以下代码段所示。

如需详细了解如何解读这些无障碍功能检查的结果,请参阅 Espresso 无障碍功能检查指南。

Kotlin

    import androidx.test.espresso.accessibility.AccessibilityChecks

    @Before
    fun setUp() {
        AccessibilityChecks.enable()
    }
    

Java

    import androidx.test.espresso.accessibility.AccessibilityChecks;

    @Before
    public void setUp() {
        AccessibilityChecks.enable();
    }
    

推动 Activity 和 Fragment 生命周期

您可以使用 ActivityScenarioFragmentScenario 类来测试应用的 Activity 和 Fragment 如何响应系统级中断和配置更改。要了解详情,请参阅关于如何测试 Activity测试 Fragment 的指南。

管理服务生命周期

AndroidX Test 包含用于管理关键服务的生命周期的代码。要了解如何定义这些规则,请参阅 JUnit4 规则指南。

评估因 SDK 版本而异的所有行为变体

如果应用的行为取决于设备的 SDK 版本,请使用 @SdkSuppress 注释,并根据应用逻辑的分叉方式传入 minSdkVersionmaxSdkVersion 的值:

Kotlin

    @Test
    @SdkSuppress(maxSdkVersion = 27)
    fun testButtonClickOnOreoAndLower() {
        // ...
    }

    @Test
    @SdkSuppress(minSdkVersion = 28)
    fun testButtonClickOnPieAndHigher() {
        // ...
    }
    

Java

    @Test
    @SdkSuppress(maxSdkVersion = 27)
    public void testButtonClickOnOreoAndLower() {
        // ...
    }

    @Test
    @SdkSuppress(minSdkVersion = 28)
    public void testButtonClickOnPieAndHigher() {
        // ...
    }
    

其他资源

如需详细了解如何在 Android 平台上进行测试,请参阅以下资源。

示例

Codelab