Espresso 测试框架 - 快速指南


Espresso 测试框架 - 简介

总的来说,移动自动化测试是一项困难且具有挑战性的任务。Android 对不同设备和平台的可用性使得移动自动化测试变得乏味。为了让它变得更容易,Google 接受了挑战并开发了 Espresso 框架。它提供了一个非常简单、一致且灵活的 API 来自动化和测试 Android 应用程序中的用户界面。Espresso 测试可以用 Java 和 Kotlin(一种用于开发 Android 应用程序的现代编程语言)编写。

Espresso API 简单易学。您可以轻松执行 Android UI 测试,而无需复杂的多线程测试。Google Drive、地图和其他一些应用程序目前正在使用 Espresso。

Espresso的特点

Espresso 支持的一些显着功能如下:

  • API 非常简单,易于学习。

  • 高度可扩展且灵活。

  • 提供单独的模块来测试 Android WebView 组件。

  • 提供单独的模块来验证和模拟 Android Intents。

  • 提供应用程序和测试之间的自动同步。

Espresso的优点

现在让我们看看Espresso的好处是什么。

  • 向后兼容性

  • 易于设置。

  • 高度稳定的测试循环。

  • 也支持应用程序外部的测试活动。

  • 支持JUnit4

  • UI 自动化适合编写黑盒测试。

Espresso 测试框架 - 设置说明

在本章中,让我们了解如何安装 espresso 框架、配置它来编写 espresso 测试并在我们的 Android 应用程序中执行它。

先决条件

Espresso 是一个用户界面测试框架,用于使用 Android SDK 测试以 Java / Kotlin 语言开发的 Android 应用程序。因此,espresso 的唯一要求是使用 Java 或 Kotlin 中的 Android SDK 开发应用程序,建议使用最新的 Android Studio。

在我们开始使用 espresso 框架之前要正确配置的项目列表如下 -

  • 安装最新的Java JDK并配置JAVA_HOME环境变量。

  • 安装最新的 Android Studio(版本 3.2 或更高版本)。

  • 使用 SDK Manager 安装最新的 Android SDK 并配置 ANDROID_HOME 环境变量。

  • 安装最新的 Gradle Build Tool 并配置 GRADLE_HOME 环境变量。

配置 EspressoTesting 框架

最初,espresso 测试框架是作为 Android 支持库的一部分提供的。随后,Android 团队提供了新的 Android 库 AndroidX,并将最新的 espresso 测试框架开发移至该库中。espresso 测试框架的最新开发(Android 9.0,API 级别 28 或更高)将在 AndroidX 库中完成。

在项目中包含 espresso 测试框架就像将 espresso 测试框架设置为应用程序 gradle 文件 app/build.gradle 中的依赖项一样简单。完整的配置如下,

使用Android支持库,

android {
   defaultConfig {
      testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
   }
}
dependencies {
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'com.android.support.test:runner:1.0.2'
   androidTestImplementation 'com.android.support.test.espresso:espressocore:3.0.2'
}

使用AndroidX库,

android {
   defaultConfig {
      testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }
}
dependencies {
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'com.androidx.test:runner:1.0.2'
   androidTestImplementation 'com.androidx.espresso:espresso-core:3.0.2'
}

android/defaultConfig中的testInstrumentationRunner设置AndroidJUnitRunner类来运行仪器测试。依赖项中的第一行包含JUnit测试框架,依赖项中的第二行包含用于运行测试用例的测试运行器库,最后依赖项中的第三行包含 espresso 测试框架。

默认情况下,Android studio 在创建 android 项目时将 espresso 测试框架(Android 支持库)设置为依赖项,gradle 将从 Maven 存储库下载必要的库。让我们创建一个简单的 Hello world android 应用程序并检查 espresso 测试框架是否配置正确。

创建新 Android 应用程序的步骤如下所述 -

  • 启动 Android Studio。

  • 选择文件 → 新建 → 新建项目。

  • 输入应用程序名称 (HelloWorldApp) 和公司域 (espressosamples.tutorialspoint.com),然后单击下一步

安卓应用

要创建 Android 项目,

  • 选择最小 API 作为 API 15:Android 4.0.3 (IceCreamSandwich),然后单击下一步。

目标 Android 设备

要定位 Android 设备,

  • 选择“清空活动”,然后单击“下一步”

空活动

要将活动添加到移动设备,

  • 输入主要活动的名称,然后单击“完成”

主要活动

要配置活动,

  • 创建新项目后,打开app/build.gradle文件并检查其内容。文件的内容指定如下,

apply plugin: 'com.android.application'
android {
   compileSdkVersion 28
   defaultConfig {
      applicationId "com.tutorialspoint.espressosamples.helloworldapp"
      minSdkVersion 15
      targetSdkVersion 28
      versionCode 1
      versionName "1.0"
      testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
   }
   buildTypes {
      release {
         minifyEnabled false
         proguardFiles getDefaultProguardFile('proguard-android.txt'),    'proguard-rules.pro'
      }
   }
}
dependencies {
   implementation fileTree(dir: 'libs', include: ['*.jar'])
   implementation 'com.android.support:appcompat-v7:28.0.0'
   implementation 'com.android.support.constraint:constraint-layout:1.1.3'
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'com.android.support.test:runner:1.0.2'
   androidTestImplementation 'com.android.support.test.espresso:espressocore:3.0.2'
}

最后一行指定 espresso 测试框架依赖项。默认情况下,配置 Android 支持库。我们可以通过单击菜单中的“重构”“迁移AndroidX”来重新配置应用程序以使用AndroidX库。

Espresso 测试框架

要迁移到 Androidx,

  • 现在,app/build.gradle的更改如下所示,

apply plugin: 'com.android.application'
android {
   compileSdkVersion 28
   defaultConfig {
      applicationId "com.tutorialspoint.espressosamples.helloworldapp"
      minSdkVersion 15
      targetSdkVersion 28
      versionCode 1
      versionName "1.0"
      testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }
   buildTypes {
      release {
         minifyEnabled false
         proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
      }
   }
}
dependencies {
   implementation fileTree(dir: 'libs', include: ['*.jar'])
   implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
   implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'androidx.test:runner:1.1.1'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

现在,最后一行包含来自 AndroidX 库的 espresso 测试框架。

设备设置

测试时,建议关闭用于测试的Android设备上的动画。这将减少检查空闲资源时的混乱。

让我们看看如何在 Android 设备上禁用动画 – (设置 → 开发者选项),

  • 窗口动画比例

  • 过渡动画比例

  • 动画师持续时间尺度

如果“设置”屏幕中没有提供“开发者选项”菜单,请多次单击“关于手机”选项中的可用版本号。这将启用“开发人员选项”菜单。

在 Android Studio 中运行测试

在本章中,让我们看看如何使用 Android studio 运行测试。

每个 Android 应用程序都有两种类型的测试 -

  • 功能/单元测试

  • 仪器测试

功能测试不需要在设备或模拟器中安装和启动实际的 Android 应用程序并测试功能。它可以在控制台本身中启动,而无需调用实际的应用程序。然而,仪器测试需要启动实际的应用程序来测试用户界面和用户交互等功能。默认情况下,单元测试编写在src/test/java/文件夹中,仪器测试编写在src/androidTest/java/文件夹中。Android studio为测试类提供了“运行”上下文菜单,以运行在所选测试类中编写的测试。默认情况下,Android应用程序有两个类:src/test文件夹中的ExampleUnitTestsrc/androidTest文件夹中的ExampleInstrumentedTest

要运行默认的单元测试,请在Android studio中选择ExampleUnitTest ,右键单击它,然后单击运行'ExampleUnitTest',如下所示,

安卓工作室

运行单元测试

这将运行单元测试并在控制台中显示结果,如以下屏幕截图所示 -

测试并展示

单元测试成功

要运行默认的仪器测试,请在android studio中选择ExampleInstrumentationTest,右键单击它,然后单击运行'ExampleInstrumentationTest',如下所示,

仪器仪表测试

运行仪器测试

这将通过在设备或模拟器中启动应用程序来运行单元测试,并在控制台中显示结果,如以下屏幕截图所示 -

单元测试

仪器测试运行成功。

Espresso 测试框架 - JUnit 概述

在本章中,让我们了解JUnit的基础知识,JUnit 是 Java 社区开发的流行单元测试框架,espresso 测试框架就是在此基础上构建的。

JUnit是 Java 应用程序单元测试的事实上的标准。尽管它在单元测试中很受欢迎,但它也为仪器测试提供了完整的支持和规定。Espresso 测试库扩展了必要的 JUnit 类以支持基于 Android 的仪器测试。

编写一个简单的单元测试

让我们创建一个 Java 类Computation (Computation.java) 并编写简单的数学运算、求和乘法然后,我们将使用JUnit编写测试用例并通过运行测试用例来检查它。

  • 启动 Android Studio。

  • 打开上一章中创建的HelloWorldApp 。

  • app/src/main/java/com/tutorialspoint/espressosamples/helloworldapp/中创建一个文件Computation.java并编写两个函数 – SumMultiply,如下所示,

package com.tutorialspoint.espressosamples.helloworldapp;
public class Computation {
   public Computation() {}
   public int Sum(int a, int b) {
      return a + b;
   }
   public int Multiply(int a, int b) {
      return a * b;
   }
}
  • 在 app/src/test/java/com/tutorialspoint/espressosamples/helloworldapp 中创建文件 ComputationUnitTest.java 并编写单元测试用例来测试 Sum 和 Multiply 功能,如下所示

package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ComputationUnitTest {
   @Test
   public void sum_isCorrect() {
      Computation computation = new Computation();
      assertEquals(4, computation.Sum(2,2));
   }
   @Test
   public void multiply_isCorrect() {
      Computation computation = new Computation();
      assertEquals(4, computation.Multiply(2,2));
   }
}

在这里,我们使用了两个新术语—— @TestassertEquals。一般来说,JUnit 使用 Java 注释来标识类中的测试用例以及有关如何执行测试用例的信息。@Test就是这样一种 Java 注释,它指定特定函数是一个 junit 测试用例。assertEquals是一个断言第一个参数(期望值)和第二个参数(计算值)相等且相同的函数。JUnit针对不同的测试场景提供了多种断言方法。

  • 现在,通过右键单击该类并调用“运行‘ComputationUnitTest’”选项,在 Android studio 中运行 ComputationUnitTest,上一章所述。这将运行单元测试用例并报告成功。

计算单元测试结果如下所示 -

计算单元测试

注释

JUnit 框架广泛使用注释。一些重要的注释如下 -

  • @测试

  • @前

  • @后

  • @课前

  • @下课以后

  • @规则

@Test注解

@Test是JUnit框架中非常重要的注解。@Test用于区分普通方法和测试用例方法。一旦一个方法被@Test注解修饰,那么该特定方法就被视为一个测试用例,并将由JUnit Runner运行。JUnit Runner是一个特殊的类,用于查找并运行java 类中可用的JUnit 测试用例。目前,我们使用Android Studio 的内置选项来运行单元测试(进而运行JUnit Runner)。示例代码如下,

package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class ComputationUnitTest {
   @Test
   public void multiply_isCorrect() {
      Computation computation = new Computation();
      assertEquals(4, computation.Multiply(2,2));
   }
}

@前

@Before注释用于引用一个方法,需要在运行特定测试类中可用的任何测试方法之前调用该方法。例如,在我们的示例中,可以在单独的方法中创建Computation对象,并使用@Before进行注释,以便它将在sum_isCorrectmultiply_isCorrect测试用例之前运行。完整代码如下,

package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class ComputationUnitTest {
   Computation computation = null;
   @Before
   public void CreateComputationObject() {
      this.computation = new Computation();
   }
   @Test
   public void sum_isCorrect() {
      assertEquals(4, this.computation.Sum(2,2));
   }
   @Test
   public void multiply_isCorrect() {
      assertEquals(4, this.computation.Multiply(2,2));
   }
}

@后

@After与@Before类似,但是用@After注解的方法将在每个测试用例运行后被调用或执行。示例代码如下,

package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class ComputationUnitTest {
   Computation computation = null;
   @Before
   public void CreateComputationObject() {
      this.computation = new Computation();
   }
   @After
   public void DestroyComputationObject() {
      this.computation = null;
   }
   @Test
   public void sum_isCorrect() {
      assertEquals(4, this.computation.Sum(2,2));
   }
   @Test
   public void multiply_isCorrect() {
      assertEquals(4, this.computation.Multiply(2,2));
   }
}

@课前

@BeforeClass与@Before类似,但是用@BeforeClass注解的方法只会在运行特定类中的所有测试用例之前调用或执行一次。创建资源密集型对象(例如数据库连接对象)很有用。这将减少执行测试用例集合的时间。此方法需要是静态的才能正常工作。在我们的示例中,我们可以在运行所有测试用例之前创建一次计算对象,如下所示,

package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class ComputationUnitTest {
   private static Computation computation = null;
   @BeforeClass
   public static void CreateComputationObject() {
      computation = new Computation();
   }
   @Test
   public void sum_isCorrect() {
      assertEquals(4, computation.Sum(2,2));
   }
   @Test
   public void multiply_isCorrect() {
      assertEquals(4, computation.Multiply(2,2));
   }
}

@下课以后

@AfterClass与@BeforeClass类似,但是用@AfterClass注解的方法只会在特定类中的所有测试用例运行后被调用或执行一次。此方法也需要静态才能正常工作。示例代码如下 -

package com.tutorialspoint.espressosamples.helloworldapp;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class ComputationUnitTest {
   private static Computation computation = null;
   @BeforeClass
   public static void CreateComputationObject() {
      computation = new Computation();
   }
   @AfterClass
   public static void DestroyComputationObject() {
      computation = null;
   }
   @Test
   public void sum_isCorrect() {
      assertEquals(4, computation.Sum(2,2));
   }
   @Test
   public void multiply_isCorrect() {
      assertEquals(4, computation.Multiply(2,2));
   }
}

@规则

@Rule注解是JUnit的亮点之一。它用于向测试用例添加Behave。我们只能注释TestRule类型的字段。它实际上提供了@Before@After注释提供的功能集,但以高效且可重用的方式提供。例如,我们可能需要一个临时文件夹来存储测试用例期间的一些数据。通常,我们需要在运行测试用例之前创建一个临时文件夹(使用@Before或@BeforeClass注释)并在测试用例运行后销毁它(使用@After或@AfterClass注释)。相反,我们可以使用JUnit框架提供的TemporaryFolder(类型为TestRule )类为所有测试用例创建一个临时文件夹,并且该临时文件夹将在测试用例运行时被删除。我们需要创建一个TemporaryFolder类型的新变量,并需要使用@Rule进行注释,如下所示,

package com.tutorialspoint.espressosamples.helloworldapp;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;

public class ComputationUnitTest {
   private static Computation computation = null;
   @Rule
   public TemporaryFolder folder = new TemporaryFolder();
   @Test
   public void file_isCreated() throws IOException {
      folder.newFolder("MyTestFolder");
      File testFile = folder.newFile("MyTestFile.txt");
      assertTrue(testFile.exists());
   }
   @BeforeClass
   public static void CreateComputationObject() {
      computation = new Computation();
   }
   @AfterClass
   public static void DestroyComputationObject() {
      computation = null;
   }
   @Test
   public void sum_isCorrect() {
      assertEquals(4, computation.Sum(2,2));
   }
   @Test
   public void multiply_isCorrect() {
      assertEquals(4, computation.Multiply(2,2));
   }
}

执行顺序

JUnit中,带有不同注解的方法将按照特定顺序执行,如下所示:

  • @课前

  • @规则

  • @前

  • @测试

  • @后

  • @下课以后

断言

断言是检查测试用例的期望值与测试用例结果的实际值是否匹配的一种方法。JUnit针对不同场景提供断言;下面列出了一些重要的断言 -

  • fail() - 显式地使测试用例失败。

  • assertTrue(boolean test_condition) - 检查 test_condition 是否为 true

  • assertFalse(boolean test_condition) - 检查 test_condition 是否为 false

  • 断言Equals(预期,实际) - 检查两个值是否相等

  • assertNull(object) - 检查对象是否为空

  • assertNotNull(object) - 检查对象是否不为空

  • assertSame(expected,actual) - 检查两者是否引用相同的对象。

  • assertNotSame(expected,actual) - 检查两者是否引用不同的对象。

Espresso 测试框架 - 架构

在本章中,让我们学习 espresso 测试框架的术语、如何编写简单的 espresso 测试用例以及 espresso 测试框架的完整工作流程或架构。

概述

Espresso 提供了大量的类来测试 Android 应用程序的用户界面和用户交互。它们可以分为以下五类 -

JUnit 运行程序

Android 测试框架提供了一个运行程序 AndroidJUnitRunner 来运行以 JUnit3 和 JUnit4 风格的测试用例编写的 espresso 测试用例。它特定于 Android 应用程序,它透明地处理在实际设备或模拟器中加载 espresso 测试用例和被测应用程序,执行测试用例并报告测试用例的结果。要在测试用例中使用 AndroidJUnitRunner,我们需要使用 @RunWith 注释来注释测试类,然后传递 AndroidJUnitRunner 参数,如下所示 -

@RunWith(AndroidJUnit4.class)
   public class ExampleInstrumentedTest {
}

JUnit 规则

Android 测试框架提供了一个规则 ActivityTestRule,用于在执行测试用例之前启动 Android 活动。它在用 @Test` 和 @Before 注释的每个方法之前启动活动。它将在用@After注释的方法之后终止活动。示例代码如下,

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

在这里,MainActivity是在运行测试用例之前启动并在特定测试用例运行之后销毁的活动。

视图匹配器

Espresso 提供了大量的视图匹配器类(在androidx.test.espresso.matcher.ViewMatchers 包中)来匹配和查找 Android 活动屏幕的视图层次结构中的 UI 元素/视图。Espresso 的 onView 方法采用Matcher (视图匹配器)类型的单个参数,查找相应的 UI 视图并返回相应的ViewInteraction对象。onView方法返回的ViewInteraction对象可以进一步用于调用诸如单击匹配视图之类的操作,或者可以用于断言匹配视图。用于查找带有文本“Hello World!”的视图的示例代码 如下,

ViewInteraction viewInteraction = Espresso.onView(withText("Hello World!"));

这里,withText是一个匹配器,它可以用来匹配具有文本“Hello World!”的UI视图。

查看动作

Espresso 提供了大量的视图操作类(在 androidx.test.espresso.action.ViewActions 中)来调用所选/匹配视图上的不同操作。一旦onView匹配并返回ViewInteraction对象,就可以通过调用ViewInteraction对象的“perform”方法来调用任何操作,并传递适当的视图操作。单击匹配视图的示例代码如下,

ViewInteraction viewInteraction = Espresso.onView(withText("Hello World!"));
viewInteraction.perform(click());

这里,将调用匹配视图的单击操作。

查看断言

与视图匹配器和视图操作类似,Espresso 提供了大量的视图断言(在androidx.test.espresso.assertion.ViewAssertions包中)来断言匹配的视图是我们所期望的。一旦 onView 匹配并返回ViewInteraction对象,就可以使用ViewInteraction的 check 方法通过传递适当的视图断言来检查任何断言。断言匹配视图的示例代码如下,

ViewInteraction viewInteraction = Espresso.onView(withText("Hello World!"));
viewInteraction.check(matches(withId(R.id.text_view)));

在这里,matches接受视图匹配器并返回视图断言,可以通过ViewInteraction的 check 方法检查。

Espresso 测试框架的工作流程

让我们了解 espresso 测试框架的工作原理以及它如何提供选项以简单灵活的方式进行任何类型的用户交互。Espresso测试用例的工作流程如下所述,

  • 正如我们之前了解到的,Android JUnit 运行器AndroidJUnit4将运行 android 测试用例。espresso 测试用例需要用@RunWith(AndroidJUnut.class)进行标记。首先,AndroidJUnit4将准备运行测试用例的环境。它启动连接的 Android 设备或模拟器,安装应用程序并确保要测试的应用程序处于就绪状态。它将运行测试用例并报告结果。

  • Espresso 至少需要一个ActivityTestRule类型的JUnit规则来指定活动。Android JUnit 运行程序将使用ActivityTestRule启动要启动的活动。

  • 每个测试用例都需要至少一个onViewonDate(用于查找基于数据的视图,如AdapterView)方法调用来匹配和查找所需的视图。onView 或 onData 返回ViewInteraction对象。

  • 返回ViewInteraction对象后,我们可以调用所选视图的操作,或者使用断言检查该视图是否为我们期望的视图。

  • 可以通过传递任何一个可用的视图操作,使用ViewInteraction对象的执行方法来调用操作。

  • 可以通过传递任何一个可用的视图断言,使用ViewInteraction对象的 check 方法来调用断言。

工作流程的图示如下:

工作流程

示例——查看断言

让我们编写一个简单的测试用例来查找包含“Hello World!”的文本视图。我们的“HelloWorldApp”应用程序中的文本,然后使用视图断言对其进行断言。完整代码如下,

package com.tutorialspoint.espressosamples.helloworldapp;

import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.withText;;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static org.junit.Assert.*;
/**
   * Instrumented test, which will execute on an Android device.
   *
   * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
   @Rule
   public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
   @Test
   public void view_isCorrect() {
      onView(withText("Hello World!")).check(matches(isDisplayed()));
   }
   @Test
   public void useAppContext() {
      // Context of the app under test.
      Context appContext = InstrumentationRegistry.getTargetContext();
      assertEquals("com.tutorialspoint.espressosamples.helloworldapp", appContext.getPackageName());
   }
}

在这里,我们使用withText视图匹配器来查找具有“Hello World!”的文本视图。text 并匹配视图断言以断言文本视图已正确显示。一旦在 Android Studio 中调用测试用例,它将运行测试用例并报告成功消息,如下所示。

view_is正确的测试用例

测试用例

Espresso 测试框架 - 视图匹配器

Espresso 框架提供了许多视图匹配器。匹配器的目的是使用视图的不同属性(例如 ID、文本和子视图的可用性)来匹配视图。每个匹配器匹配视图的特定属性并应用于特定类型的视图。例如,withId匹配器匹配视图的Id属性并适用于所有视图,而 withText 匹配器匹配视图的Text属性并仅适用于TextView

在本章中,让我们学习 espresso 测试框架提供的不同匹配器,并了解构建 espresso 匹配器的Hamcrest库。

汉克雷斯特图书馆

Hamcrest库是 espresso 测试框架范围内的一个重要库。Hamcrest本身就是一个用于编写匹配器对象的框架。Espresso 框架广泛使用Hamcrest库,并在必要时对其进行扩展,以提供简单且可扩展的匹配器。

Hamcrest提供了一个简单的函数assertThat和一组匹配器来断言任何对象。assertThat有三个参数,如下所示 -

  • 字符串(测试的描述,可选)

  • 对象(实际)

  • 匹配器(预期)

让我们编写一个简单的示例来测试列表对象是否具有预期值。

import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.MatcherAssert.assertThat;
@Test
public void list_hasValue() {
   ArrayList<String> list = new ArrayList<String>();
   list.add("John");
   assertThat("Is list has John?", list, hasItem("John"));
}

这里,hasItem返回一个匹配器,它检查实际列表是否具有指定值作为其中一项。

Hamcrest有很多内置匹配器,也有创建新匹配器的选项。在Espresso测试框架中有用的一些重要的内置匹配器如下 -

任何事情 - 总是匹配者

基于逻辑的匹配器

  • allOf - 接受任意数量的匹配器,并且仅当所有匹配器都成功时才匹配。

  • anyOf - 接受任意数量的匹配器,如果任何一个匹配器成功则匹配。

  • not - 接受一个匹配器,并且仅当匹配器失败时才匹配,反之亦然。

基于文本的匹配器

  • equalToIgnoringCase - 用于测试实际输入是否等于预期的字符串忽略大小写。

  • equalToIgnoringWhiteSpace - 用于测试实际输入是否等于指定的字符串,忽略大小写和空格。

  • containsString - 用于测试实际输入是否包含指定字符串。

  • endsWith - 用于测试实际输入是否以指定字符串开头。

  • startsWith - 用于测试实际输入是否以指定字符串结尾。

基于数字的匹配器

  • closeTo - 用于测试实际输入是否接近预期数字。

  • GreaterThan - 用于测试实际输入是否大于预期数字。

  • GreaterThanOrEqualTo - 用于测试实际输入是否大于或等于预期数字。

  • lessThan - 用于测试实际输入是否小于预期数字。

  • lessThanOrEqualTo - 用于测试实际输入是否小于或等于预期数字。

基于对象的匹配器

  • equalTo - 用于测试实际输入是否等于预期对象

  • hasToString - 用于测试实际输入是否具有 toString 方法。

  • instanceOf - 用于测试实际输入是否是预期类的实例。

  • isCompatibleType - 用于测试实际输入是否与预期类型兼容。

  • notNullValue - 用于测试实际输入是否不为空。

  • SameInstance - 用于测试实际输入和预期输入是否属于同一实例。

  • hasProperty - 用于测试实际输入是否具有预期属性

is - 糖或equalTo的捷径

匹配器

Espresso 提供了 onView() 方法来匹配和查找视图。它接受视图匹配器并返回 ViewInteraction 对象以与匹配的视图进行交互。常用的视图匹配器列表如下所述 -

withId()

withId()接受 int 类型的参数,该参数引用视图的 id。它返回一个匹配器,该匹配器使用视图的 id 来匹配视图。示例代码如下,

onView(withId(R.id.testView))

withText()

withText()接受字符串类型的参数,该参数引用视图的文本属性的值。它返回一个匹配器,该匹配器使用视图的文本值来匹配视图。它仅适用于TextView。示例代码如下,

onView(withText("Hello World!"))

withContentDescription()

withContentDescription()接受字符串类型的参数,该参数引用视图的内容描述属性的值。它返回一个匹配器,该匹配器使用视图的描述来匹配视图。示例代码如下,

onView(withContentDescription("blah"))

我们还可以传递文本值的资源 ID,而不是文本本身。

onView(withContentDescription(R.id.res_id_blah))

有内容描述()

hasContentDescription()没有参数。它返回一个匹配器,该匹配器与具有任何内容描述的视图相匹配。示例代码如下,

onView(allOf(withId(R.id.my_view_id), hasContentDescription()))

withTagKey()

withTagKey()接受字符串类型的参数,该参数引用视图的标签键。它返回一个匹配器,该匹配器使用其标签键来匹配视图。示例代码如下,

onView(withTagKey("blah"))

我们还可以传递标签名称的资源 ID,而不是标签名称本身。

onView(withTagKey(R.id.res_id_blah))

withTagValue()

withTagValue()接受 Matcher <Object> 类型的参数,该参数引用视图的标签值。它返回一个匹配器,该匹配器使用其标签值来匹配视图。示例代码如下,

onView(withTagValue(is((Object) "blah")))

这里Hamcrest 匹配器。

withClassName()

withClassName()接受 Matcher<String> 类型的参数,该参数引用视图的类名值。它返回一个匹配器,该匹配器使用其类名来匹配视图。示例代码如下,

onView(withClassName(endsWith("EditText")))

这里,endsWith是 Hamcrest 匹配器并返回 Matcher<String>

withHint()

withHint()接受 Matcher<String> 类型的参数,该参数引用视图的提示值。它返回一个匹配器,该匹配器使用视图的提示来匹配视图。示例代码如下,

onView(withClassName(endsWith("Enter name")))

withInputType()

withInputType()接受int类型的参数,该参数引用视图的输入类型。它返回一个匹配器,该匹配器使用其输入类型来匹配视图。示例代码如下,

onView(withInputType(TYPE_CLASS_DATETIME))

这里,TYPE_CLASS_DATETIME指的是支持日期和时间的编辑视图。

withResourceName()

withResourceName()接受 Matcher<String> 类型的参数,该参数引用视图的类名值。它返回一个匹配器,该匹配器使用视图的资源名称来匹配视图。示例代码如下,

onView(withResourceName(endsWith("res_name")))

它也接受字符串参数。示例代码如下,

onView(withResourceName("my_res_name"))

withAlpha()

withAlpha()接受float类型的参数,该参数引用视图的 alpha 值。它返回一个匹配器,该匹配器使用视图的 alpha 值来匹配视图。示例代码如下,

onView(withAlpha(0.8))

withEffectiveVisibility()

withEffectiveVisibility()接受ViewMatchers.Visibility类型的参数,该参数引用视图的有效可见性。它返回一个匹配器,该匹配器使用视图的可见性来匹配视图。示例代码如下,

onView(withEffectiveVisibility(withEffectiveVisibility.INVISIBLE))

withSpinnerText()

withSpinnerText()接受 Matcher<String> 类型的参数,该参数引用 Spinner 当前选定视图的值。它返回一个匹配器,该匹配器根据所选项目的 toString 值来匹配微调器。示例代码如下,

onView(withSpinnerText(endsWith("USA")))

它也接受字符串参数或字符串的资源 ID。示例代码如下,

onView(withResourceName("USA"))
onView(withResourceName(R.string.res_usa))

withSubstring()

withSubString()与withText()类似,只是它有助于测试视图文本值的子字符串。

onView(withSubString("Hello"))

有链接()

hasLinks()没有参数,它返回一个匹配器,该匹配器与具有链接的视图相匹配。它仅适用于 TextView。示例代码如下,

onView(allOf(withSubString("Hello"), hasLinks()))

在这里,allOf是 Hamcrest 匹配器。allOf返回一个匹配器,它匹配所有传入的匹配器,在这里,它用于匹配视图以及检查视图的文本值中是否有链接。

有文本颜色()

hasTextColor()接受一个 int 类型的参数,该参数引用颜色的资源 id。它返回一个匹配器,该匹配器根据TextView的颜色进行匹配。它仅适用于TextView。示例代码如下,

onView(allOf(withSubString("Hello"), hasTextColor(R.color.Red)))

hasEllipsizedText()

hasEllipsizedText()没有参数。它返回一个匹配器,该匹配器与具有长文本且椭圆形(第一个..十个..最后一个)或截断(第一个...)的 TextView 匹配。示例代码如下,

onView(allOf(withId(R.id.my_text_view_id), hasEllipsizedText()))

有多行文本()

hasMultilineText()没有参数。它返回一个匹配器,该匹配器与具有任何多行文本的 TextView 匹配。示例代码如下,

onView(allOf(withId(R.id.my_test_view_id), hasMultilineText()))

有背景()

hasBackground()接受一个 int 类型的参数,该参数引用后台资源的资源 id。它返回一个匹配器,该匹配器根据视图的背景资源来匹配视图。示例代码如下,

onView(allOf(withId("image"), hasBackground(R.drawable.your_drawable)))

有错误文本()

hasErrorText()接受 Matcher<String> 类型的参数,该参数引用视图的 (EditText) 错误字符串值。它返回一个匹配器,该匹配器使用视图的错误字符串来匹配视图。这仅适用于EditText。示例代码如下,

onView(allOf(withId(R.id.editText_name), hasErrorText(is("name is required"))))

它也接受字符串参数。示例代码如下,

onView(allOf(withId(R.id.editText_name), hasErrorText("name is required")))

hasImeAction()

hasImeAction()接受 Matcher<Integer> 类型的参数,该参数引用视图 (EditText) 支持的输入方法。它返回一个匹配器,该匹配器使用视图支持的输入方法来匹配视图。这仅适用于EditText。示例代码如下,

onView(allOf(withId(R.id.editText_name),
hasImeAction(is(EditorInfo.IME_ACTION_GO))))

此处,EditorInfo.IME_ACTION_GO 是输入法选项之一。hasImeAction()也接受整数参数。示例代码如下,

onView(allOf(withId(R.id.editText_name),
hasImeAction(EditorInfo.IME_ACTION_GO)))

支持输入方法()

supportsInputMethods()没有参数。它返回一个匹配器,如果视图支持输入法,则该匹配器与视图相匹配。示例代码如下,

onView(allOf(withId(R.id.editText_name), supportsInputMethods()))

是根()

isRoot()没有参数。它返回一个与根视图匹配的匹配器。示例代码如下,

onView(allOf(withId(R.id.my_root_id), isRoot()))

被展示()

isDisplayed()没有参数。它返回一个匹配器,该匹配器与当前显示的视图相匹配。示例代码如下,

onView(allOf(withId(R.id.my_view_id), isDisplayed()))

isDisplayingAtLeast()

isDisplayingAtLeast()接受一个 int 类型的参数。它返回一个匹配器,该匹配器与当前显示的视图至少匹配指定的百分比。示例代码如下,

onView(allOf(withId(R.id.my_view_id), isDisplayingAtLeast(75)))

完全显示()

isCompletelyDisplayed()没有参数。它返回一个匹配器,该匹配器与当前完全显示在屏幕上的视图相匹配。示例代码如下,

onView(allOf(withId(R.id.my_view_id), isCompletelyDisplayed()))

已启用()

isEnabled()没有参数。它返回一个匹配器,该匹配器与启用的视图相匹配。示例代码如下,

onView(allOf(withId(R.id.my_view_id), isEnabled()))

isFocusable()

isFocusable()没有参数。它返回一个匹配器,该匹配器与具有焦点选项的视图相匹配。示例代码如下,

onView(allOf(withId(R.id.my_view_id), isFocusable()))

有焦点()

hasFocus()没有参数。它返回一个匹配器,该匹配器与当前聚焦的视图相匹配。示例代码如下,

onView(allOf(withId(R.id.my_view_id), hasFocus()))

可点击()

isClickable()没有参数。它返回一个匹配器,该匹配器与单击选项的视图相匹配。示例代码如下,

onView(allOf(withId(R.id.my_view_id), isClickable()))

isSelected()

isSelected()没有参数。它返回一个匹配器,该匹配器与当前选择的视图相匹配。示例代码如下,

onView(allOf(withId(R.id.my_view_id), isSelected()))

已检查()

isChecked()没有参数。它返回一个匹配器,该匹配器与复合按钮类型(或其子类型)且处于选中状态的视图相匹配。示例代码如下,

onView(allOf(withId(R.id.my_view_id), isChecked()))

未检查()

isNotChecked()与 isChecked 正好相反。示例代码如下*所示,

onView(allOf(withId(R.id.my_view_id), isNotChecked()))

isJavascriptEnabled()

isJavascriptEnabled()没有参数。它返回一个匹配器,该匹配器与正在评估 JavaScript 的 WebView 相匹配。示例代码如下,

onView(allOf(withId(R.id.my_webview_id), isJavascriptEnabled()))

withParent()

withParent()接受一个 Matcher<View> 类型的参数。论证指的是一种观点。它返回一个匹配器,该匹配器匹配指定视图是父视图的视图。示例代码如下,

onView(allOf(withId(R.id.childView), withParent(withId(R.id.parentView))))

有兄弟姐妹()

hasSibling()接受一个类型为 Matcher>View< 的参数。论证指的是一种观点。它返回一个匹配器,该匹配器匹配传入视图是其同级视图之一的视图。示例代码如下,

onView(hasSibling(withId(R.id.siblingView)))

withChild()

withChild()接受一个 Matcher<View> 类型的参数。论证指的是一种观点。它返回一个匹配器,该匹配器匹配传入视图是子视图的视图。示例代码如下,

onView(allOf(withId(R.id.parentView), withChild(withId(R.id.childView))))

hasChildCount()

hasChildCount()接受一个 int 类型的参数。该参数指的是视图的子项计数。它返回一个匹配器,该匹配器与具有与参数中指定的子视图数量完全相同的视图进行匹配。示例代码如下,

onView(hasChildCount(4))

hasMinimumChildCount()

hasMinimumChildCount()接受一个 int 类型的参数。该参数指的是视图的子项计数。它返回一个匹配器,该匹配器匹配至少具有参数中指定的子视图数量的视图。示例代码如下,

onView(hasMinimumChildCount(4))

具有后代()

hasDescendant()接受一个 Matcher<View> 类型的参数。论证指的是一种观点。它返回一个匹配器,该匹配器匹配传入视图是视图层次结构中的后代视图之一的视图。示例代码如下,

onView(hasDescendant(withId(R.id.descendantView)))

isDescendantOfA()

isDescendantOfA()接受一个 Matcher<View> 类型的参数。论证指的是一种观点。它返回一个匹配器,该匹配器匹配传入视图是视图层次结构中的祖先视图之一的视图。示例代码如下,

onView(allOf(withId(R.id.myView), isDescendantOfA(withId(R.id.parentView))))

自定义视图匹配器

Espresso 提供了各种选项来创建我们自己的自定义视图匹配器,它基于Hamcrest匹配器。自定义匹配器是一个非常强大的概念,可以扩展框架,也可以根据我们的口味定制框架。编写自定义匹配器的一些优点如下:

  • 利用我们自己的自定义视图的独特功能

  • 自定义匹配器有助于基于AdapterView 的测试用例与不同类型的底层数据进行匹配。

  • 通过结合多个匹配器的特征来简化当前的匹配器

当需求出现时,我们可以创建新的匹配器,这非常容易。让我们创建一个新的自定义匹配器,它返回一个匹配器来测试TextView的 id 和文本。

Espresso 提供以下两个类来编写新的匹配器 -

  • 类型安全匹配器

  • 有界匹配器

这两个类本质上相似,只是BoundedMatcher透明地处理将对象转换为正确的类型,而无需手动检查正确的类型。我们将使用BoundedMatcher类创建一个新的匹配器withIdAndText。让我们检查一下编写新匹配器的步骤。

  • 在app/build.gradle文件中添加以下依赖项并同步它。

dependencies {
   implementation 'androidx.test.espresso:espresso-core:3.1.1'
}
  • 创建一个新类以包含我们的匹配器(方法)并将其标记为最终类

public final class MyMatchers {
}
  • 使用必要的参数在新类中声明一个静态方法,并将 Matcher<View> 设置为返回类型。

public final class MyMatchers {
   @NonNull
   public static Matcher<View> withIdAndText(final Matcher<Integer>
   integerMatcher, final Matcher<String> stringMatcher) {
   }
}
  • 在静态方法中使用以下签名创建一个新的 BoundedMatcher 对象(也返回值),

public final class MyMatchers {
   @NonNull
   public static Matcher<View> withIdAndText(final Matcher<Integer>
   integerMatcher, final Matcher<String> stringMatcher) {
      return new BoundedMatcher<View, TextView>(TextView.class) {
      };
   }
}
  • 重写BoundedMatcher对象中的describeTomatchesSafely方法。describeTo 有一个类型为Description的参数,没有返回类型,它用于有关匹配器的错误信息。matchesSafely有一个 TextView 类型的参数,返回类型为boolean,用于匹配视图。

最终版本的代码如下:

public final class MyMatchers {
   @NonNull
   public static Matcher<View> withIdAndText(final Matcher<Integer>
   integerMatcher, final Matcher<String> stringMatcher) {
      return new BoundedMatcher<View, TextView>(TextView.class) {
         @Override
         public void describeTo(final Description description) {
            description.appendText("error text: ");
            stringMatcher.describeTo(description);
            integerMatcher.describeTo(description);
         }
         @Override
         public boolean matchesSafely(final TextView textView) {
            return stringMatcher.matches(textView.getText().toString()) &&
            integerMatcher.matches(textView.getId());
         }
      };
   }
}
  • 最后,我们可以使用 mew 匹配器来编写测试用例,如下所示,

@Test
public void view_customMatcher_isCorrect() {
   onView(withIdAndText(is((Integer) R.id.textView_hello), is((String) "Hello World!")))
      .check(matches(withText("Hello World!")));
}

Espresso 测试框架 - 查看断言

如前所述,视图断言用于断言实际视图(使用视图匹配器找到)和预期视图相同。示例代码如下,

onView(withId(R.id.my_view)) .check(matches(withText("Hello")))

这里,

  • onView()返回与匹配视图对应的ViewInteration对象。ViewInteraction用于与匹配的视图进行交互。

  • withId(R.id.my_view)返回一个视图匹配器,它将与id属性等于my_view 的视图(实际)匹配。

  • withText(“Hello”)还返回一个视图匹配器,它将与文本属性等于Hello 的视图(预期)进行匹配。

  • check是一种接受ViewAssertion类型的参数并使用传入的ViewAssertion对象进行断言的方法。

  • matches(withText(“Hello”))返回一个视图断言,它将真正断言实际视图(使用withId找到)和预期视图(使用withText找到)是相同的。

让我们学习 espresso 测试框架提供的一些断言视图对象的方法。

不存在()

返回一个视图断言,它确保视图匹配器找不到任何匹配的视图。

onView(withText("Hello")) .check(doesNotExist());

此处,测试用例确保不存在带有文本 Hello 的视图。

火柴()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并与目标视图匹配器匹配的视图匹配。

onView(withId(R.id.textView_hello)) .check(matches(withText("Hello World!")));

这里,测试用例确保具有 id R.id.textView_hello 的视图存在并与带有文本 Hello World! 的目标视图匹配。

isBottomAlignedWith()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且与目标视图匹配器底部对齐。

onView(withId(R.id.view)) .check(isBottomAlignedWith(withId(R.id.target_view)))

这里,测试用例确保具有 id R.id.view的视图存在并且与具有 id R.id.target_view的视图底部对齐。

isCompletelyAbove()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且完全位于目标视图匹配器之上。

onView(withId(R.id.view)) .check(isCompletelyAbove(withId(R.id.target_view)))

这里,测试用例确保具有 id、R.id.view 的视图存在并且完全位于具有 id、R.id.target_view 的视图之上

isCompletelyBelow()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且完全位于目标视图匹配器下方。

onView(withId(R.id.view)) .check(isCompletelyBelow(withId(R.id.target_view)))

这里,测试用例确保具有 id R.id.view的视图存在并且完全位于具有 id R.id.target_view的视图下方。

isCompletelyLeftOf()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且完全位于目标视图匹配器的左侧。

onView(withId(R.id.view)) .check(isCompletelyLeftOf(withId(R.id.target_view)))

这里,测试用例确保具有 id、R.id.view 的视图存在并且完全位于具有 id、R.id.target_view的视图的左侧

isCompletelyRightOf()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且完全位于目标视图匹配器的右侧。

onView(withId(R.id.view)) .check(isCompletelyRightOf(withId(R.id.target_view)))

这里,测试用例确保具有 id、R.id.view 的视图存在并且完全位于具有 id、R.id.target_view 的视图的右侧。

isLeftAlignedWith()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且与目标视图匹配器左对齐。

onView(withId(R.id.view)) .check(isLeftAlignedWith(withId(R.id.target_view)))

这里,测试用例确保具有 id、R.id.view 的视图存在,并且与具有 id、 R.id.target_view 的视图左对齐

isPartiallyAbove()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且部分位于目标视图匹配器之上。

onView(withId(R.id.view)) .check(isPartiallyAbove(withId(R.id.target_view)))

这里,测试用例确保具有 id、R.id.view 的视图存在并且部分位于具有 id、 R.id.target_view 的视图上方

isPartiallyBelow()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且部分位于目标视图匹配器下方。

onView(withId(R.id.view)) .check(isPartiallyBelow(withId(R.id.target_view)))

这里,测试用例确保具有 id R.id.view的视图存在并且部分位于具有 id R.id.target_view的视图下方。

isPartiallyLeftOf()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且部分位于目标视图匹配器的左侧。

onView(withId(R.id.view)) .check(isPartiallyLeftOf(withId(R.id.target_view)))

这里,测试用例确保具有 id R.id.view 的视图存在并且部分位于具有 id R.id.target_view的视图的左侧。

isPartiallyRightOf()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且部分位于目标视图匹配器的右侧

onView(withId(R.id.view)) .check(isPartiallyRightOf(withId(R.id.target_view)))

这里,测试用例确保具有 id R.id.view 的视图存在并且部分位于具有 id R.id.target_view的视图的右侧。

isRightAlignedWith()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且与目标视图匹配器右对齐。

onView(withId(R.id.view)) .check(isRightAlignedWith(withId(R.id.target_view)))

这里,测试用例确保具有 id R.id.view的视图存在并且与具有 id R.id.target_view的视图右对齐。

isTopAlignedWith()

接受目标视图匹配器并返回视图断言,这确保视图匹配器(实际)存在并且与目标视图匹配器顶部对齐。

onView(withId(R.id.view)) .check(isTopAlignedWith(withId(R.id.target_view)))

这里,测试用例确保具有 id、R.id.view 的视图存在并且与具有 id、 R.id.target_view 的视图顶部对齐

noEllipsizedText()

返回一个视图断言,它确保视图层次结构不包含省略或截断的文本视图。

onView(withId(R.id.view)) .check(noEllipsizedText());

没有多行按钮()

返回一个视图断言,它确保视图层次结构不包含多行按钮。

onView(withId(R.id.view)) .check(noMultilineButtons());

没有重叠()

返回一个视图断言,它确保后代对象可分配给 TextView 或