Android 测试(一)

概述

测试是App整体开发流程的中的重要一部分,通过运行测试,可以在发布应用之前充分验证应用的正确性,功能特性和可用性。

测试还有以下优点:

  • 对错误的快速反馈。
  • 在开发周期中的早期错误检测。
  • 更安全的代码重构,安心的优化代码而不必担心回归。
  • 稳定的开发速度,帮助减少技术债。

 

基本测试原理

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

 

构建代码进行测试


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

迭代地创建和测试代码

在迭代开发功能时,可以先编写一个新测试,也可以将示例和断言添加到现有的单元测试中。测试刚开始会失败,因为该功能尚未实现。

在设计新功能时考虑出现的功能单元非常重要。对于每个单元,编写相应的单元测试。您的单元测试应该几乎覆盖与设备的所有可能的交互,包括标准交互,无效输入和资源不可用的情况。建议尽可能利用Jetpack库;当使用这些经过良好测试的库时,可以专注于验证应用的功能特性。

Android 测试(一)_第1张图片

图1:以上两个循环与迭代,测试驱动的开发相关联

完整的工作流程,如图1所示,包含一系列嵌套的迭代循环,其中包含一个长而缓慢的UI驱动循环测试代码单元的集成。我们可以使用更短,更快的开发周期自行测试单元。这组循环测试一直持续到应用程序满足每个用例为止。

将应用视为一系列模块

为了使代码更易于测试,建议根据组件化来开发代码,其中每个模块代表用户在应用程序中完成的特定任务。这个视角与应用程序的基于堆栈的视图形成对比,该应用通常包含表示UI层,业务逻辑层和数据层。

例如,“任务列表”应用程序可能具有用于创建任务,查看已完成任务的统计信息以及将照片与特定任务相关联的模块。这种模块化体系结构还可以帮助我们将不相关的类分离,并为开发团队分配所有权提供了一种自然的结构。

在每个模块周围设置明确定义的边界,并在应用程序规模和复杂性增长时创建新模块非常重要。每个模块应该只有一个关注区域,并且模块间通信的API应该是一致的。为了更轻松,更快速地测试这些模块间交互,建议创建模块的空实现。在测试中,一个模块的真实实现可以调用另一个模块的空实现。

然而,当我们创建一个新模块时,不要教条式地要求它马上具有完整的功能。对于一个特定的模块,没有一个或多个层级,是没有关系的。

要了解有关如何在应用程序中定义模块的更多信息,以及有关创建和发布模块的平台支持,请参阅Android App Bundles。

 

配置您的测试环境


在配置环境和依赖关系以在应用中创建测试时,请遵循本节下面介绍的最佳方法。

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

Android Studio中的典型项目包含两个放置测试的目录。按如下方式组织测试:

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

 

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

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

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

真实设备提供最高保真度,但也花费大量时间来运行测试。另一方面,模拟设备以较低保真度为代价提供改进的测试速度。然而,该平台在二进制资源和现实技术中的改进使得模拟设备能够产生更真实的结果。

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

考虑是否使用测试副本

创建测试时,可以选择创建真实对象或测试副本,例如假对象或模拟对象。通常,在测试中使用真实对象比使用测试副本更好,尤其是当测试对象满足以下条件之一时:

  • 该对象是一个数据对象。
  • 除非它与依赖项的真实对象通信,否则该对象无法运行。一个很好的例子是事件回调处理程序。
  • 使用依赖项复制对象的通信很困难。一个很好的例子是SQL数据库处理程序,其中内存数据库可伪造比数据库更健壮的测试。
  • 特别是,模拟我们不拥有的类型实例通常会导致脆弱的测试,只有当我们了解了其他人对该类型的实现的复杂性时才会起作用。仅使用此类模拟作为最后的手段。模拟自己的对象是可以的,但请记住,使用@Spy注释的模拟比模拟类中所有功能的模拟提供更高的保真度。

但是,如果您的测试尝试在真实对象上执行以下类型的操作,则最好创建伪造或甚至模拟对象:

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

提示:请与库作者核实,看看他们是否提供了可以可靠依赖的任何官方支持的测试基础架构,例如仿造。

 

编写测试


在配置测试环境之后,可以开始编写评估应用程序功能的测试了。本节介绍如何编写小型,中型和大型测试。


测试金字塔的级别

金字塔包含三层

Android 测试(一)_第2张图片
图2.测试金字塔,显示应该包含在应用程序测试套件中的三类测试
测试金字塔,如图2所示,说明了您的应用程序应如何包含三类测试:小型,中型和大型:

 

  • 小测试是单元测试,可以一次验证一个类的程序功能。
  • 中等测试是集成测试,用于验证模块内层级之间的交互,或相关模块之间的交互。
  • 大型测试是端到端测试,用于验证跨越应用程序的多个模块的用户交互。

当处理金字塔时,从小型测试到大型测试,每次测试都会提高保真度,但也会增加执行时间和维护和调试的工作量。因此,应该编写比集成测试更多的单元测试,以及比端到端测试更多的集成测试。虽然每个类别的测试比例可能因应用程序的使用情况而异,但我们通常建议在类别中进行以下划分:70%小测试,20%中等测试,10%大测试。

编写小测试

编写的小测试应该是高度集中的单元测试,可以详尽地验证应用程序中每个类的功能和约束。

在特定类中添加和更改方法时,建议针对它们创建并运行单元测试。如果这些测试依赖于Android框架,请使用统一的,与设备无关的API,例如androidx.test API。这样就可以在没有物理设备或模拟器的情况下在本地运行测试。

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

app/build.gradle

android {
    // ...

    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
}

备注:Android Studio 3.4及更高版本默认提供资源的编译版本。

本地单元测试

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

Robolectric模拟Android 4.1(API级别16)或更高版本的运行时,并提供称为阴影的社区维护副本。此功能可以让测试只依赖于这个框架,而无需使用模拟器或模拟对象。 Robolectric支持Android平台的以下方面:

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

仪器单元测试

我们可以在物理设备或仿真器上运行已检测的单元测试。但是,这种形式的测试涉及的执行时间明显慢于本地单元测试,因此,只有在评估应用程序对实际设备硬件的行为至关重要时才最好使用此方法。

运行检测测试时,AndroidX Test使用以下线程:

  • 主线程,也称为“UI线程”或“活动线程”,其中发生UI交互和活动生命周期事件。
  • 检测线程,大多数测试程序运行的线程。当测试程序开始时,AndroidJUnitTest类将启动此线程。

如果需要在主线程上执行测试,请使用@UiThreadTest对其进行注释。

编写中等测试

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

让应用程序结构和以下中等测试示例(按增加范围的顺序)来定义表示应用程序中单元组的最佳方式:

  • view和view model之间的交互,例如测试Fragment对象,验证布局XML或评估ViewModel对象的数据绑定逻辑。
  • 在应用程序的存储库层中进行测试,以验证您的不同数据源和数据访问对象(DAO)是否按预期进行交互。
  • 应用程序的垂直切片,测试特定屏幕上的交互。此类测试会验证应用程序各层的交互。
  • 多个Fragment测试,用于评估应用的特定区域。与此列表中提到的其他类型的介质测试不同,此类测试通常需要真实设备,因为测试中的交互涉及多个UI元素。

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

使用Espresso Intents库中的方法。要简化传递给这些测试的信息,请使用副本和存根。
结合使用IntentSubject和基于Truth的断言来验证捕获的意图。
运行仪器介质测试时使用Espresso
当您在设备或Robolectric上执行类似于以下的UI交互时,Espresso可帮助保持任务同步:

  • 对View对象执行操作。
  • 评估具有辅助功能需求的用户如何使用您的应用。
  • 在RecyclerView和AdapterView对象中查找和激活项目。
  • 验证传出意图的状态。
  • 验证WebView对象中的DOM结构。

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

编写大型测试

虽然单独测试应用程序中的每个类和模块非常重要,但验证引导用户完成多个模块和功能的端到端工作流同样重要。这些类型的测试在代码中形成了不可避免的瓶颈,但我们可以通过验证尽可能接近实际成品的应用程序来最小化此影响。

注意:对于编写的每个基于工作流的大型测试,您还应编写中等测试,以检查工作流中包含的每个模块的功能。该测试结构使得更容易确定关键用户旅程的哪个步骤表现出意外行为。
如果您的应用程序足够小,您可能只需要一套大型测试来评估整个应用程序的功能。否则,您应该按团队所有权,功能垂直或用户目标划分大型测试套件。

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

Espresso中的同步支持
除了支持中型仪器测试外,Espresso还在大型测试中完成以下任务时提供同步支持:

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

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

 

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


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

使用Truth创建更可读的断言

Guava团队提供了一个名为Truth的流畅断言库。在构建验证步骤或测试步骤时,您可以使用此库作为基于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的断言更容易构建:

  • BundleSubject
  • IntentSubject
  • MotionEventSubject
  • NotificationActionSubject
  • NotificationSubject
  • PendingIntentSubject
  • PointerCoordsSubject
  • PointerPropertiesSubject

AndroidX Test API可帮助您执行与移动应用程序测试相关的常见任务,以下各节将对此进行讨论。

编写UI测试

Espresso可以以线程安全的方式以编程方式定位应用程序中的UI元素并与之交互。要了解更多信息,请参阅Espresso指南。

运行UI测试

AndroidJUnitRunner类定义了一个基于检测的JUnit测试运行器,它允许在Android设备上运行JUnit 3或JUnit 4样式的测试类。测试运行器有助于将测试包和被测应用程序加载到设备或模拟器上,运行测试并报告结果。

要进一步提高这些测试的可靠性,请使用Android Test Orchestrator,它在自己的Instrumentation沙箱中运行每个UI测试。此体系结构可减少测试之间的共享状态,并在每个测试的基础上隔离应用程序崩溃。有关Android Test Orchestrator在测试应用时提供的好处的详细信息,请参阅Android Test Orchestrator指南。


与可见元素交互

UI Automator API允许您与设备上的可见元素进行交互,无论具有焦点的activity或fragment如何。

警告:仅建议在应用必须与系统UI或其他应用交互以满足关键用例时才使用UI Automator测试您的应用。由于UI Automator与特定系统UI交互,因此您必须在每个平台版本升级后以及每个新版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
fun setUp() {
    AccessibilityChecks.enable()
}

驱动activity和fragment生命周期

使用ActivityScenario和FragmentScenario类来测试应用程序的activity和fragment如何响应系统级中断和配置更改。要了解更多信息,请参阅有关如何测试activity和fragment片段的指南。

注意:考虑到fragment表现的特定于设备的条件,最好将FragmentScenario作为仪器化测试的一部分。

管理服务生命周期

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

评估SDK版本不同的所有功能变体

如果您的应用的功能取决于设备的SDK版本,请使用@SdkSuppress注释,传入minSdkVersion或maxSdkVersion的值,具体取决于您如何分支应用的逻辑:

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上进行测试的更多信息,请参阅以下资源。

例子
Android测试例子


Codelabs
Android 测试 Codelab

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Android开发相关,Android测试)