- Espresso 测试框架教程
- Espresso 测试 - 首页
- 介绍
- 设置说明
- 在 Android Studio 中运行测试
- JUnit 概述
- 架构
- 视图匹配器 (View Matchers)
- 自定义视图匹配器 (Custom View Matchers)
- 视图断言 (View Assertions)
- 视图操作 (View Actions)
- 测试 AdapterView
- 测试 WebView
- 测试异步操作
- 测试 Intent
- 测试多个应用程序的 UI
- 测试录制器 (Test Recorder)
- 测试 UI 性能
- 测试辅助功能
- Espresso 测试资源
- Espresso 测试 - 快速指南
- Espresso 测试 - 有用资源
- Espresso 测试 - 讨论
Espresso 测试框架 - 快速指南
Espresso 测试框架 - 介绍
总的来说,移动自动化测试是一项困难且具有挑战性的任务。Android 在不同设备和平台上的可用性使得移动自动化测试变得繁琐。为了简化这一过程,Google 接受了这一挑战并开发了 Espresso 框架。它提供了一个非常简单、一致且灵活的 API 来自动化和测试 Android 应用程序中的用户界面。Espresso 测试可以用 Java 和 Kotlin(一种用于开发 Android 应用程序的现代编程语言)编写。
Espresso API 简单易学。您可以轻松地执行 Android UI 测试,而无需多线程测试的复杂性。Google Drive、地图和其他一些应用程序目前正在使用 Espresso。
Espresso 的特性
Espresso 支持的一些主要特性如下:
非常简单的 API,易于学习。
高度可扩展和灵活。
提供单独的模块来测试 Android WebView 组件。
提供单独的模块来验证和模拟 Android Intent。
提供应用程序和测试之间的自动同步。
Espresso 的优势
让我们来看看 Espresso 的好处。
向后兼容性
易于设置。
高度稳定的测试周期。
也支持测试应用程序外部的活动。
支持 JUnit4
适用于编写黑盒测试的 UI 自动化。
Espresso 测试框架 - 设置说明
在本章中,让我们了解如何安装 Espresso 框架,将其配置为编写 Espresso 测试并在我们的 Android 应用程序中执行它。
先决条件
Espresso 是一个用户界面测试框架,用于测试使用 Android SDK 以 Java/Kotlin 语言开发的 Android 应用程序。因此,Espresso 的唯一要求是使用 Android SDK 以 Java 或 Kotlin 开发应用程序,建议使用最新的 Android Studio。
在我们开始使用 Espresso 框架之前,需要正确配置以下各项:
安装最新的 Java JDK 并配置 JAVA_HOME 环境变量。
安装最新的 Android Studio(3.2 或更高版本)。
使用 SDK Manager 安装最新的 Android SDK 并配置 ANDROID_HOME 环境变量。
安装最新的 Gradle Build Tool 并配置 GRADLE_HOME 环境变量。
配置 Espresso 测试框架
最初,Espresso 测试框架作为 Android Support 库的一部分提供。后来,Android 团队提供了一个新的 Android 库 AndroidX,并将最新的 Espresso 测试框架开发迁移到该库。最新的 Espresso 测试框架开发(Android 9.0,API 级别 28 或更高)将在 AndroidX 库中进行。
在项目中包含 Espresso 测试框架就像在应用程序 gradle 文件 app/build.gradle 中将 Espresso 测试框架设置为依赖项一样简单,完整的配置如下:
使用 Android Support 库:
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 类来运行 Instrumentation 测试。dependencies 中的第一行包含 JUnit 测试框架,第二行包含运行测试用例的测试运行器库,最后第三行包含 Espresso 测试框架。
默认情况下,Android Studio 在创建 Android 项目时将 Espresso 测试框架(Android Support 库)设置为依赖项,Gradle 将从 Maven 存储库下载必要的库。让我们创建一个简单的 Hello world Android 应用程序,并检查 Espresso 测试框架是否已正确配置。
创建新 Android 应用程序的步骤如下:
启动 Android Studio。
选择 File → New → New Project。
输入应用程序名称 (HelloWorldApp) 和公司域名 (espressosamples.tutorialspoint.com),然后单击 Next。
创建 Android 项目:
选择最小 API 为 API 15:Android 4.0.3 (IceCreamSandwich),然后单击 Next。
选择目标 Android 设备:
选择 Empty Activity,然后单击 Next。
向移动设备添加活动:
输入主活动的名称,然后单击 Finish。
配置活动:
创建新项目后,打开 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 Support 库。我们可以通过单击菜单中的 Refactor → Migrate to AndroidX 来重新配置应用程序以使用 AndroidX 库。
迁移到 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 应用程序都有两种类型的测试:
功能/单元测试
Instrumentation 测试
功能测试不需要在设备或模拟器中安装和启动实际的 Android 应用程序,并测试其功能。它可以在控制台中本身启动,而无需调用实际的应用程序。但是,Instrumentation 测试需要启动实际应用程序才能测试功能,例如用户界面和用户交互。默认情况下,单元测试写在 **src/test/java/** 文件夹中,Instrumentation 测试写在 **src/androidTest/java/** 文件夹中。Android Studio 为测试类提供 Run 上下文菜单,以运行在所选测试类中编写的测试。默认情况下,Android 应用程序有两个类:src/test 文件夹中的 ExampleUnitTest 和 src/androidTest 文件夹中的 ExampleInstrumentedTest。
要运行默认单元测试,请在 Android Studio 中选择 ExampleUnitTest,右键单击它,然后单击“运行 'ExampleUnitTest'”,如下所示:
运行单元测试
这将运行单元测试并在控制台中显示结果,如下面的屏幕截图所示:
单元测试成功
要运行默认 Instrumentation 测试,请在 Android Studio 中选择 ExampleInstrumentationTest,右键单击它,然后单击“运行 'ExampleInstrumentationTest'”,如下所示:
运行 Instrumentation 测试
这将通过在设备或模拟器中启动应用程序来运行单元测试,并在控制台中显示结果,如下面的屏幕截图所示:
Instrumentation 测试运行成功。
Espresso 测试框架 - JUnit 概述
在本章中,让我们了解 JUnit 的基础知识,JUnit 是 Java 社区开发的流行单元测试框架,Espresso 测试框架就是基于它构建的。
JUnit 是 Java 应用程序单元测试的事实标准。尽管它流行于单元测试,但它也完全支持 Instrumentation 测试。Espresso 测试库扩展了必要的 JUnit 类以支持基于 Android 的 Instrumentation 测试。
编写简单的单元测试
让我们创建一个 Java 类 Computation(Computation.java),并编写简单的数学运算 Summation 和 Multiplication。然后,我们将使用 JUnit 编写测试用例,并通过运行测试用例来检查它。
启动 Android Studio。
打开上一章中创建的 HelloWorldApp。
在 app/src/main/java/com/tutorialspoint/espressosamples/helloworldapp/ 中创建一个文件 Computation.java,并编写两个函数 – Sum 和 Multiply,如下所示:
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)); } }
在这里,我们使用了两个新术语 – @Test 和 assertEquals。一般来说,JUnit 使用 Java 注解来识别类中的测试用例以及如何执行测试用例的信息。@Test 是一个这样的 Java 注解,它指定特定函数是 JUnit 测试用例。assertEquals 是一个函数,用于断言第一个参数(预期值)和第二个参数(计算值)相等且相同。JUnit 为不同的测试场景提供许多断言方法。
现在,通过右键单击该类并调用“运行 'ComputationUnitTest'”选项(如上一章所述)来在 Android Studio 中运行 ComputationUnitTest。这将运行单元测试用例并报告成功。
计算单元测试的结果如下所示:
注解
JUnit 框架广泛使用注解。一些重要的注解如下:
@Test
@Before
@After
@BeforeClass
@AfterClass
@Rule
@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
@Before 注解用于引用一个方法,该方法需要在运行特定测试类中任何测试方法之前调用。例如,在我们的示例中,可以在单独的方法中创建 Computation 对象并用 @Before 注解,以便它在 sum_isCorrect 和 multiply_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
@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
@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
@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
@Rule 注解是 JUnit 的亮点之一。它用于向测试用例添加行为。我们只能注解 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 中,用不同注解注释的方法将按如下所示的特定顺序执行:
@BeforeClass
@Rule
@Before
@Test
@After
@AfterClass
断言
断言是一种检查测试用例的预期值是否与测试用例结果的实际值匹配的方法。JUnit 为不同的场景提供了断言;下面列出了一些重要的断言:
fail() − 显式地使测试用例失败。
assertTrue(boolean test_condition) − 检查 test_condition 是否为真
assertFalse(boolean test_condition) − 检查 test_condition 是否为假
assertEquals(expected, actual) − 检查两个值是否相等
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 是在运行测试用例之前启动并在特定测试用例运行后销毁的活动。
ViewMatchers
Espresso 提供大量 ViewMatcher 类(在 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 视图。
ViewActions
Espresso 提供大量 ViewAction 类(在 androidx.test.espresso.action.ViewActions 中)来对所选/匹配的视图调用不同的操作。一旦 onView 匹配并返回 ViewInteraction 对象,就可以通过调用 ViewInteraction 对象的“perform”方法并使用适当的视图操作传递它来调用任何操作。点击匹配视图的示例代码如下:
ViewInteraction viewInteraction = Espresso.onView(withText("Hello World!")); viewInteraction.perform(click());
这里,将调用匹配视图的点击操作。
ViewAssertions
与视图匹配器和视图操作类似,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 启动要启动的活动。
每个测试用例至少需要调用一次 onView 或 onDate(用于查找基于数据的视图,例如 AdapterView)方法来匹配和查找所需的视图。onView 或 onData 返回 ViewInteraction 对象。
一旦返回 ViewInteraction 对象,我们就可以调用所选视图的操作,或者使用断言检查视图的预期视图。
可以使用 ViewInteraction 对象的 perform 方法通过传递任何一个可用的视图操作来调用操作。
可以使用 ViewInteraction 对象的 check 方法通过传递任何一个可用的视图断言来调用断言。
工作流程 的图表表示如下:
示例 - 视图断言
让我们编写一个简单的测试用例来查找我们的“HelloWorldApp”应用程序中具有“Hello World!”文本的文本视图,然后使用视图断言对其进行断言。完整的代码如下:
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!”文本的文本视图,并使用 matches 视图断言来断言文本视图是否正确显示。一旦在 Android Studio 中调用测试用例,它将运行测试用例并报告如下成功消息。
view_isCorrect 测试用例
Espresso 测试框架 - 视图匹配器
Espresso 框架提供许多视图匹配器。匹配器的目的是使用视图的不同属性(如 Id、文本和子视图的可用性)来匹配视图。每个匹配器都匹配视图的特定属性并应用于特定类型的视图。例如,withId 匹配器匹配视图的 Id 属性并应用于所有视图,而 withText 匹配器匹配视图的 Text 属性并仅应用于 TextView。
在本章中,让我们学习 Espresso 测试框架提供的不同匹配器,以及学习 Espresso 匹配器构建的基础 Hamcrest 库。
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 测试框架中很有用的重要内置匹配器如下:
anything - 总是匹配
基于逻辑的匹配器
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()
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")))
这里,is 是 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 值来匹配 spinner。示例代码如下:
onView(withSpinnerText(endsWith("USA")))
它也接受字符串参数或字符串的资源 ID。示例代码如下:
onView(withResourceName("USA")) onView(withResourceName(R.string.res_usa))
withSubstring()
withSubString() 与 withText() 类似,不同之处在于它有助于测试视图文本值的子字符串。
onView(withSubString("Hello"))
hasLinks()
hasLinks() 没有参数,它返回一个匹配器,该匹配器匹配具有链接的视图。仅适用于 TextView。示例代码如下:
onView(allOf(withSubString("Hello"), hasLinks()))
这里,allOf 是一个 Hamcrest 匹配器。allOf 返回一个匹配器,该匹配器匹配所有传入的匹配器,在这里,它用于匹配视图以及检查视图的文本值中是否包含链接。
hasTextColor()
hasTextColor() 接受一个 int 类型参数,该参数指的是颜色的资源 ID。它返回一个匹配器,该匹配器根据其颜色来匹配 TextView。仅适用于 TextView。示例代码如下:
onView(allOf(withSubString("Hello"), hasTextColor(R.color.Red)))
hasEllipsizedText()
hasEllipsizedText() 没有参数。它返回一个匹配器,该匹配器匹配具有长文本且已省略号显示(first.. ten.. last)或被截断(first…)的 TextView。示例代码如下:
onView(allOf(withId(R.id.my_text_view_id), hasEllipsizedText()))
hasMultilineText()
hasMultilineText() 没有参数。它返回一个匹配器,该匹配器匹配具有任何多行文本的 TextView。示例代码如下:
onView(allOf(withId(R.id.my_test_view_id), hasMultilineText()))
hasBackground()
hasBackground() 接受一个 int 类型参数,该参数指的是背景资源的资源 ID。它返回一个匹配器,该匹配器根据其背景资源来匹配视图。示例代码如下:
onView(allOf(withId("image"), hasBackground(R.drawable.your_drawable)))
hasErrorText()
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()
supportsInputMethods() 没有参数。如果视图支持输入方法,则它返回一个匹配器,该匹配器匹配该视图。示例代码如下:
onView(allOf(withId(R.id.editText_name), supportsInputMethods()))
isRoot()
isRoot() 没有参数。它返回一个匹配器,该匹配器匹配根视图。示例代码如下:
onView(allOf(withId(R.id.my_root_id), isRoot()))
isDisplayed()
isDisplayed() 没有参数。它返回一个匹配器,该匹配器匹配当前显示的视图。示例代码如下:
onView(allOf(withId(R.id.my_view_id), isDisplayed()))
isDisplayingAtLeast()
isDisplayingAtLeast() 接受一个 int 类型参数。它返回一个匹配器,该匹配器匹配当前至少显示指定百分比的视图。示例代码如下:
onView(allOf(withId(R.id.my_view_id), isDisplayingAtLeast(75)))
isCompletelyDisplayed()
isCompletelyDisplayed() 没有参数。它返回一个匹配器,该匹配器匹配当前完全显示在屏幕上的视图。示例代码如下:
onView(allOf(withId(R.id.my_view_id), isCompletelyDisplayed()))
isEnabled()
isEnabled() 没有参数。它返回一个匹配器,该匹配器匹配已启用的视图。示例代码如下:
onView(allOf(withId(R.id.my_view_id), isEnabled()))
isFocusable()
isFocusable() 没有参数。它返回一个匹配器,该匹配器匹配具有焦点选项的视图。示例代码如下:
onView(allOf(withId(R.id.my_view_id), isFocusable()))
hasFocus()
hasFocus() 没有参数。它返回一个匹配器,该匹配器匹配当前获得焦点的视图。示例代码如下:
onView(allOf(withId(R.id.my_view_id), hasFocus()))
isClickable()
isClickable() 没有参数。它返回一个匹配器,该匹配器匹配具有单击选项的视图。示例代码如下:
onView(allOf(withId(R.id.my_view_id), isClickable()))
isSelected()
isSelected() 没有参数。它返回一个匹配器,该匹配器匹配当前选定的视图。示例代码如下:
onView(allOf(withId(R.id.my_view_id), isSelected()))
isChecked()
isChecked() 没有参数。它返回一个匹配器,该匹配器匹配类型为 CompoundButton(或其子类型)且处于选中状态的视图。示例代码如下:
onView(allOf(withId(R.id.my_view_id), isChecked()))
isNotChecked()
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()
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()
hasDescendant() 接受一个 Matcher<View> 类型参数。该参数指的是一个视图。它返回一个匹配器,该匹配器匹配传入视图是视图层次结构中后代视图之一的视图。示例代码如下:
onView(hasDescendant(withId(R.id.descendantView)))
isDescendantOfA()
isDescendantOfA() 接受一个 Matcher<View> 类型的参数。该参数指向一个视图。它返回一个匹配器,该匹配器匹配传入视图在其视图层次结构中祖先视图之一的视图。示例代码如下:
onView(allOf(withId(R.id.myView), isDescendantOfA(withId(R.id.parentView))))
自定义视图匹配器 (Custom View Matchers)
Espresso 提供了各种创建自定义视图匹配器的选项,它基于 Hamcrest 匹配器。自定义匹配器是一个非常强大的概念,可以扩展框架,也可以根据我们的喜好自定义框架。编写自定义匹配器的一些优点如下:
利用我们自己的自定义视图的独特功能
自定义匹配器有助于基于 AdapterView 的测试用例与不同类型的底层数据匹配。
通过组合多个匹配器的功能来简化当前的匹配器
我们可以根据需要创建新的匹配器,而且非常容易。让我们创建一个新的自定义匹配器,它返回一个匹配器来测试 TextView 的 ID 和文本。
Espresso 提供以下两个类来编写新的匹配器:
TypeSafeMatcher
BoundedMatcher
这两个类本质上相似,只是 BoundedMatcher 透明地处理对象的类型转换到正确的类型,而无需手动检查正确的类型。我们将使用 BoundedMatcher 类创建一个新的匹配器 withIdAndText。让我们检查编写新匹配器的步骤。
在 app/build.gradle 文件中添加以下依赖项并同步。
dependencies { implementation 'androidx.test.espresso:espresso-core:3.1.1' }
创建一个新类来包含我们的匹配器(方法),并将其标记为 final
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 对象中的 describeTo 和 matchesSafely 方法。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()); } }; } }
最后,我们可以使用新的匹配器来编写测试用例,如下所示:
@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 测试框架提供的一些用于断言视图对象的方法。
doesNotExist()
返回一个视图断言,确保视图匹配器找不到任何匹配的视图。
onView(withText("Hello")) .check(doesNotExist());
在这里,测试用例确保没有文本为 Hello 的视图。
matches()
接受目标视图匹配器并返回一个视图断言,确保视图匹配器(实际)存在并与目标视图匹配器匹配的视图匹配。
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());
noMultilineButtons()
返回一个视图断言,确保视图层次结构不包含多行按钮。
onView(withId(R.id.view)) .check(noMultilineButtons());
noOverlaps()
返回一个视图断言,确保可分配给 TextView 或 ImageView 的后代对象不会相互重叠。它还有另一个选项,它接受一个目标视图匹配器并返回一个视图断言,确保与目标视图匹配的后代视图不会重叠。
Espresso 测试框架 - 视图操作
如前所述,视图操作自动化 Android 应用程序中用户可以执行的所有可能的动作。Espresso 的 onView 和“onData”提供 perform 方法,该方法接受视图操作并调用/自动化测试环境中的相应用户操作。例如,“click()”是一个视图操作,当传递给 onView(R.id.myButton).perform(click()) 方法时,它将在测试环境中触发按钮(ID:“myButton”)的单击事件。
在本章中,让我们学习 Espresso 测试框架提供的视图操作。
typeText()
typeText() 接受一个 String 类型的参数(文本)并返回一个视图操作。返回的视图操作将提供的文本输入视图。在放置文本之前,它会点击视图一次。如果视图已经包含文本,则内容可能被放置在任意位置。
onView(withId(R.id.text_view)).perform(typeText("Hello World!"))
typeTextIntoFocusedView()
typeTextIntoFocusedView() 与 typeText() 类似,只是它将文本放置在视图中光标位置的右侧。
onView(withId(R.id.text_view)).perform(typeTextIntoFocusedView("Hello World!"))
replaceText()
replaceText() 与 typeText() 类似,只是它替换视图的内容。
onView(withId(R.id.text_view)).perform(typeTextIntoFocusedView("Hello World!"))
clearText()
clearText() 没有参数,返回一个视图操作,它将清除视图中的文本。
onView(withId(R.id.text_view)).perform(clearText())
pressKey()
pressKey() 接受键码(例如 KeyEvent.KEYCODE_ENTER)并返回一个视图操作,它将按下与键码对应的键。
onView(withId(R.id.text_view)).perform(typeText( "Hello World!", pressKey(KeyEvent.KEYCODE_ENTER))
pressMenuKey()
pressMenuKey() 没有参数,返回一个视图操作,它将按下硬件菜单键。
onView(withId(R.id.text_view)).perform(typeText( "Hello World!", pressKey(KeyEvent.KEYCODE_ENTER), pressMenuKey())
closeSoftKeyboard()
closeSoftKeyboard() 没有参数,返回一个视图操作,如果键盘已打开,它将关闭键盘。
onView(withId(R.id.text_view)).perform(typeText( "Hello World!", closeSoftKeyboard())
click()
click() 没有参数,返回一个视图操作,它将调用视图的单击操作。
onView(withId(R.id.button)).perform(click())
doubleClick()
doubleClick() 没有参数,返回一个视图操作,它将调用视图的双击操作。
onView(withId(R.id.button)).perform(doubleClick())
longClick()
longClick() 没有参数,返回一个视图操作,它将调用视图的长按操作。
onView(withId(R.id.button)).perform(longClick())
pressBack()
pressBack() 没有参数,返回一个视图操作,它将点击后退按钮。
onView(withId(R.id.button)).perform(pressBack())
pressBackUnconditionally()
pressBackUnconditionally() 没有参数,返回一个视图操作,它将点击后退按钮,并且如果后退按钮操作退出应用程序本身,则不会抛出异常。
onView(withId(R.id.button)).perform(pressBack())
openLink()
openLink() 方法有两个参数。第一个参数(链接文本)类型为 Matcher,引用 HTML 锚标签的文本。第二个参数(URL)类型为 Matcher,引用 HTML 锚标签的 URL。此方法仅适用于 TextView。它返回一个视图操作,该操作收集文本视图内容中所有可用的 HTML 锚标签,查找与第一个参数(链接文本)和第二个参数(URL)匹配的锚标签,最后打开相应的 URL。让我们考虑一个文本视图,其内容如下:
<a href="http://www.google.com/">copyright</a>
然后,可以使用下面的测试用例打开并测试链接:
onView(withId(R.id.text_view)).perform(openLink(is("copyright"), is(Uri.parse("http://www.google.com/"))))
这里,openLink 将获取文本视图的内容,查找文本为“copyright”,URL 为 www.google.com 的链接,并在浏览器中打开该 URL。
openLinkWithText()
openLinkWithText() 有一个参数,该参数可以是 **String** 类型或 Matcher 类型。它只是 openLink 方法的快捷方式。
onView(withId(R.id.text_view)).perform(openLinkWithText("copyright"))
openLinkWithUri()
openLinkWithUri() 有一个参数,该参数可以是 String 类型或 Matcher 类型。它只是 openLink 方法的快捷方式。
onView(withId(R.id.text_view)).perform(openLinkWithUri("http://www.google.com/"))
pressImeActionButton()
pressImeActionButton() 没有参数,并返回一个视图操作,该操作将执行在 android:imeOptions 配置中设置的操作。例如,如果 android:imeOptions 等于 actionNext,这将把光标移动到屏幕上下一个可用的 EditText 视图。
onView(withId(R.id.text_view)).perform(pressImeActionButton())
scrollTo()
scrollTo() 没有参数,并返回一个视图操作,该操作将滚动屏幕上匹配的 scrollView。
onView(withId(R.id.scrollView)).perform(scrollTo())
swipeDown()
swipeDown() 没有参数,并返回一个视图操作,该操作将在屏幕上触发向下滑动操作。
onView(withId(R.id.root)).perform(swipeDown())
swipeUp()
swipeUp() 没有参数,并返回一个视图操作,该操作将在屏幕上触发向上滑动操作。
onView(withId(R.id.root)).perform(swipeUp())
swipeRight()
swipeRight() 没有参数,并返回一个视图操作,该操作将在屏幕上触发向右滑动操作。
onView(withId(R.id.root)).perform(swipeRight())
swipeLeft()
swipeLeft() 没有参数,并返回一个视图操作,该操作将在屏幕上触发向左滑动操作。
onView(withId(R.id.root)).perform(swipeLeft())
Espresso 测试框架 - AdapterView
AdapterView 是一种特殊的视图,专门设计用于呈现类似信息的集合,例如使用 Adapter 从底层数据源获取的产品列表和用户联系人。数据源可以是简单的列表到复杂数据库条目。一些从 AdapterView 派生的视图是 ListView、GridView 和 Spinner。
AdapterView 根据底层数据源中可用数据的数量动态地呈现用户界面。此外,AdapterView 只呈现屏幕可用可见区域内可以呈现的最小必要数据。AdapterView 这样做是为了节省内存,即使底层数据很大,也能使用户界面看起来流畅。
分析表明,AdapterView 架构的性质使得 onView 选项及其视图匹配器变得无关紧要,因为首先可能根本没有呈现要测试的特定视图。幸运的是,espresso 提供了一个方法 onData(),它接受 hamcrest 匹配器(与底层数据的类型相关)来匹配底层数据,并返回对应于匹配数据视图的 DataInteraction 类型对象。示例代码如下所示:
onData(allOf(is(instanceOf(String.class)), startsWith("Apple"))).perform(click())
在这里,onData() 匹配条目“Apple”,如果它在底层数据(数组列表)中可用,则返回 DataInteraction 对象以与匹配的视图(对应于“Apple”条目的 TextView)进行交互。
方法
DataInteraction 提供以下方法与视图交互:
perform()
这接受视图操作并触发传入的视图操作。
onData(allOf(is(instanceOf(String.class)), startsWith("Apple"))).perform(click())
check()
这接受视图断言并检查传入的视图断言。
onData(allOf(is(instanceOf(String.class)), startsWith("Apple"))) .check(matches(withText("Apple")))
inAdapterView()
这接受视图匹配器。它根据传入的视图匹配器选择特定的 AdapterView,并返回 DataInteraction 对象以与匹配的 AdapterView 进行交互。
onData(allOf()) .inAdapterView(withId(R.id.adapter_view)) .atPosition(5) .perform(click())
atPosition()
这接受一个整数类型参数,并引用底层数据中项目的在位置。它选择与传入的数据位置值对应的视图,并返回 DataInteraction 对象以与匹配的视图进行交互。如果我们知道底层数据的正确顺序,这将很有用。
onData(allOf()) .inAdapterView(withId(R.id.adapter_view)) .atPosition(5) .perform(click())
onChildView()
这接受视图匹配器并匹配特定子视图内的视图。例如,我们可以与基于 AdapterView 的产品列表中的特定项目(如“购买”按钮)进行交互。
onData(allOf(is(instanceOf(String.class)), startsWith("Apple"))) .onChildView(withId(R.id.buy_button)) .perform(click())
编写示例应用程序
按照以下步骤编写基于 AdapterView 的简单应用程序,并使用 onData() 方法编写测试用例。
启动 Android Studio。
创建新的项目,如前所述,并将其命名为 MyFruitApp。
使用 Refactor → Migrate to AndroidX 选项菜单将应用程序迁移到 AndroidX 框架。
删除主活动中的默认设计并添加 ListView。activity_main.xml 的内容如下:
<?xml version = "1.0" encoding = "utf-8"?> <RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:app = "http://schemas.android.com/apk/res-auto" xmlns:tools = "http://schemas.android.com/tools" android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = ".MainActivity"> <ListView android:id = "@+id/listView" android:layout_width = "wrap_content" android:layout_height = "wrap_content" /> </RelativeLayout>
添加新的布局资源 item.xml 以指定列表视图的项目模板。item.xml 的内容如下:
<?xml version = "1.0" encoding = "utf-8"?> <TextView xmlns:android = "http://schemas.android.com/apk/res/android" android:id = "@+id/name" android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:padding = "8dp" />
现在,创建一个适配器,其水果数组作为底层数据,并将其设置为列表视图。这需要在 MainActivity 的 onCreate() 中完成,如下所示:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Find fruit list view final ListView listView = (ListView) findViewById(R.id.listView); // Initialize fruit data String[] fruits = new String[]{ "Apple", "Banana", "Cherry", "Dates", "Elderberry", "Fig", "Grapes", "Grapefruit", "Guava", "Jack fruit", "Lemon", "Mango", "Orange", "Papaya", "Pears", "Peaches", "Pineapple", "Plums", "Raspberry", "Strawberry", "Watermelon" }; // Create array list of fruits final ArrayList<String> fruitList = new ArrayList<String>(); for (int i = 0; i < fruits.length; ++i) { fruitList.add(fruits[i]); } // Create Array adapter final ArrayAdapter adapter = new ArrayAdapter(this, R.layout.item, fruitList); // Set adapter in list view listView.setAdapter(adapter); }
现在,编译代码并运行应用程序。My Fruit App 的屏幕截图如下:
现在,打开 ExampleInstrumentedTest.java 文件并添加 ActivityTestRule,如下所示:
@Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class);
此外,确保在 app/build.gradle 中完成了测试配置:
dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:rules:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' }
添加一个新的测试用例来测试列表视图,如下所示:
@Test public void listView_isCorrect() { // check list view is visible onView(withId(R.id.listView)).check(matches(isDisplayed())); onData(allOf(is(instanceOf(String.class)), startsWith("Apple"))).perform(click()); onData(allOf(is(instanceOf(String.class)), startsWith("Apple"))) .check(matches(withText("Apple"))); // click a child item onData(allOf()) .inAdapterView(withId(R.id.listView)) .atPosition(10) .perform(click()); }
最后,使用 Android Studio 的上下文菜单运行测试用例,并检查所有测试用例是否成功。
Espresso 测试框架 - WebView
WebView 是 Android 提供的一种特殊视图,用于在应用程序内显示网页。WebView 没有提供像 Chrome 和 Firefox 这样的成熟浏览器应用程序的所有功能。但是,它提供了对要显示的内容的完全控制,并公开了所有可以在网页内调用的 Android 功能。它启用 WebView 并提供了一个特殊的环境,可以使用 HTML 技术和本地功能(如相机和拨打电话)轻松设计 UI。此功能集使 WebView 能够提供一种称为 混合应用程序 的新型应用程序,其中 UI 使用 HTML 完成,业务逻辑使用 JavaScript 或通过外部 API 端点完成。
通常情况下,测试 WebView 是一项挑战,因为它使用 HTML 技术而不是原生用户界面/视图来创建其用户界面元素。Espresso 在这方面表现出色,因为它提供了一组新的 Web 匹配器和 Web 断言,这些匹配器和断言在设计上类似于原生视图匹配器和视图断言。同时,它还通过包含基于 Web 技术的测试环境提供了一种均衡的方法。
Espresso Web 基于 WebDriver Atom 框架构建,该框架用于查找和操作 Web 元素。Atom 类似于视图操作。Atom 将执行网页内的所有交互。WebDriver 公开了预定义的一组方法,如 findElement()、getElement(),用于查找 Web 元素并返回相应的 atom(在网页中执行操作)。
标准 Web 测试语句如下所示:
onWebView() .withElement(Atom) .perform(Atom) .check(WebAssertion)
这里:
onWebView() - 类似于 onView(),它公开了一组用于测试 WebView 的 API。
withElement() - 使用 Atom 在网页内查找 Web 元素的几种方法之一,并返回 WebInteration 对象,这类似于 ViewInteraction。
perform() - 使用 Atom 在网页内执行操作并返回 WebInteraction。
check() - 使用 WebAssertion 执行必要的断言。
一个示例 Web 测试代码如下所示:
onWebView() .withElement(findElement(Locator.ID, "apple")) .check(webMatches(getText(), containsString("Apple")))
这里:
findElement() 定位元素并返回一个 Atom
webMatches 类似于 matches 方法
编写示例应用程序
让我们编写一个基于 WebView 的简单应用程序,并使用 onWebView() 方法编写测试用例。请按照以下步骤编写示例应用程序:
启动 Android Studio。
创建新的项目,如前所述,并将其命名为 MyWebViewApp。
使用 Refactor → Migrate to AndroidX 选项菜单将应用程序迁移到 AndroidX 框架。
在 AndroidManifest.xml 文件中添加以下配置选项以授予访问 Internet 的权限。
<uses-permission android:name = "android.permission.INTERNET" />
Espresso Web 作为单独的插件提供。因此,请在 app/build.gradle 中添加依赖项并同步。
dependencies { androidTestImplementation 'androidx.test:rules:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.1' }
删除主活动中的默认设计并添加 WebView。activity_main.xml 的内容如下:
<?xml version = "1.0" encoding = "utf-8"?> <RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:app = "http://schemas.android.com/apk/res-auto" xmlns:tools = "http://schemas.android.com/tools" android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = ".MainActivity"> <WebView android:id = "@+id/web_view_test" android:layout_width = "fill_parent" android:layout_height = "fill_parent" /> </RelativeLayout>
创建一个新类 ExtendedWebViewClient,扩展 WebViewClient 并覆盖 shouldOverrideUrlLoading 方法,以便在同一个 WebView 中加载链接操作;否则,它将在应用程序外部打开一个新的浏览器窗口。将其放在 MainActivity.java 中。
private class ExtendedWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } }
现在,在 MainActivity 的 onCreate 方法中添加以下代码。代码的目的是查找 WebView,正确配置它,然后最后加载目标 URL。
// Find web view WebView webView = (WebView) findViewById(R.id.web_view_test); // set web view client webView.setWebViewClient(new ExtendedWebViewClient()); // Clear cache webView.clearCache(true); // load Url webView.loadUrl("http://<your domain or IP>/index.html");
这里:
index.html 的内容如下:
<html> <head> <title>Android Web View Sample</title> </head> <body> <h1>Fruits</h1> <ol> <li><a href = "apple.html" id = "apple">Apple</a></li> <li><a href = "banana.html" id = "banana">Banana</a></li> </ol> </body> </html>
index.html 中引用的 apple.html 文件的内容如下:
<html> <head> <title>Android Web View Sample</title> </head> <body> <h1>Apple</h1> </body> </html>
banana.html 文件中引用的 banana.html 文件的内容如下:
<html> <head> <title>Android Web View Sample</title> </head> <body> <h1>Banana</h1> </body> </html>
将 index.html、apple.html 和 banana.html 放置在 Web 服务器中
将 loadUrl 方法中的 URL 替换为您配置的 URL。
现在,运行应用程序并手动检查一切是否正常。以下是 WebView 示例应用程序 的屏幕截图:
现在,打开 ExampleInstrumentedTest.java 文件并添加以下规则:
@Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class, false, true) { @Override protected void afterActivityLaunched() { onWebView(withId(R.id.web_view_test)).forceJavascriptEnabled(); } };
在这里,我们找到了 WebView 并启用了 WebView 的 JavaScript,因为 espresso Web 测试框架专门通过 JavaScript 引擎来识别和操作 Web 元素。
现在,添加测试用例来测试我们的 WebView 及其行为。
@Test public void webViewTest(){ onWebView() .withElement(findElement(Locator.ID, "apple")) .check(webMatches(getText(), containsString("Apple"))) .perform(webClick()) .withElement(findElement(Locator.TAG_NAME, "h1")) .check(webMatches(getText(), containsString("Apple"))); }
在这里,测试按以下顺序进行:
使用 findElement() 方法和 Locator.ID 枚举通过其 id 属性找到链接 apple。
使用 webMatches() 方法检查链接的文本。
对链接执行单击操作。它将打开 apple.html 页面。
再次使用 findElement() 方法和 Locator.TAG_NAME 枚举找到 h1 元素。
最后再次使用 webMatches() 方法检查 h1 标签的文本。
最后,使用 Android Studio 上下文菜单运行测试用例。
异步操作
在本章中,我们将学习如何使用 Espresso Idling Resources 测试异步操作。
现代应用程序面临的挑战之一是提供流畅的用户体验。提供流畅的用户体验需要在后台进行大量工作,以确保应用程序流程不超过几毫秒。后台任务范围从简单的任务到从远程 API/数据库获取数据的复杂且耗时的任务。
如果开发多线程应用程序很复杂,那么编写其测试用例就更加复杂。例如,在从数据库加载必要数据之前,我们不应该测试AdapterView。如果在单独的线程中获取数据,则测试需要等到线程完成。因此,测试环境应在后台线程和 UI 线程之间同步。Espresso 为测试多线程应用程序提供了出色的支持。应用程序使用线程的方式如下,Espresso 支持每种场景。
用户界面线程
Android SDK 内部使用它来为复杂的 UI 元素提供流畅的用户体验。Espresso 透明地支持此场景,不需要任何配置和特殊编码。
异步任务
现代编程语言支持异步编程,可以在不增加线程编程复杂性的情况下进行轻量级线程处理。Espresso 框架也透明地支持异步任务。
用户线程
开发人员可以启动一个新线程来从数据库中获取复杂或大型数据。为了支持这种情况,Espresso 提供了空闲资源的概念。
让我们在本节学习空闲资源的概念以及如何使用它。
概述
空闲资源的概念非常简单直观。其基本思想是在单独的线程中启动长时间运行的进程时创建一个变量(布尔值),以识别该进程是否正在运行,并在测试环境中注册它。在测试期间,测试运行器将检查注册的变量(如果找到),然后查找其运行状态。如果运行状态为真,测试运行器将等待直到状态变为假。
Espresso 提供了一个接口IdlingResources
,用于维护运行状态。要实现的主要方法是isIdleNow()
。如果isIdleNow()
返回 true,Espresso 将恢复测试过程;否则,将等待直到isIdleNow()
返回 false。我们需要实现IdlingResources
并使用派生类。Espresso 还提供了一些内置的IdlingResources
实现以减轻我们的工作量。它们如下所示:
CountingIdlingResource
这维护了一个正在运行的任务的内部计数器。它公开了increment()
和decrement()
方法。increment()
将计数器加一,decrement()
从计数器中减一。只有在没有活动任务时,isIdleNow()
才返回 true。
UriIdlingResource
这类似于CountingIdlingResource
,只是计数器需要为零一段时间以考虑网络延迟。
IdlingThreadPoolExecutor
这是ThreadPoolExecutor
的自定义实现,用于维护当前线程池中活动运行任务的数量。
IdlingScheduledThreadPoolExecutor
这类似于IdlingThreadPoolExecutor
,但它也调度任务,并且是ScheduledThreadPoolExecutor
的自定义实现。
如果在应用程序中使用了上述任何一个IdlingResources
实现或自定义实现,则在使用IdlingRegistry
类测试应用程序之前,也需要将其注册到测试环境中,如下所示:
IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource());
此外,测试完成后可以将其移除,如下所示:
IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource());
Espresso 在单独的包中提供此功能,需要在 app.gradle 中配置该包,如下所示:
dependencies { implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1' androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1" }
示例应用程序
让我们创建一个简单的应用程序,通过在单独的线程中从 Web 服务获取水果列表,然后使用空闲资源的概念对其进行测试。
启动 Android Studio。
创建一个新项目,如前所述,并将其命名为 MyIdlingFruitApp
使用“重构”→“迁移到 AndroidX”选项菜单将应用程序迁移到 AndroidX 框架。
在 app/build.gradle 中添加 Espresso 空闲资源库(并同步),如下所示:
dependencies { implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1' androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1" }
删除主活动中的默认设计并添加 ListView。activity_main.xml 的内容如下所示:
<?xml version = "1.0" encoding = "utf-8"?> <RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:app = "http://schemas.android.com/apk/res-auto" xmlns:tools = "http://schemas.android.com/tools" android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = ".MainActivity"> <ListView android:id = "@+id/listView" android:layout_width = "wrap_content" android:layout_height = "wrap_content" /> </RelativeLayout>
添加新的布局资源 item.xml 以指定列表视图的项目模板。item.xml 的内容如下:
<?xml version = "1.0" encoding = "utf-8"?> <TextView xmlns:android = "http://schemas.android.com/apk/res/android" android:id = "@+id/name" android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:padding = "8dp" />
创建一个新类 - MyIdlingResource。MyIdlingResource 用于在一个地方保存我们的 IdlingResource,并在需要时获取它。我们将在示例中使用 CountingIdlingResource。
package com.tutorialspoint.espressosamples.myidlingfruitapp; import androidx.test.espresso.IdlingResource; import androidx.test.espresso.idling.CountingIdlingResource; public class MyIdlingResource { private static CountingIdlingResource mCountingIdlingResource = new CountingIdlingResource("my_idling_resource"); public static void increment() { mCountingIdlingResource.increment(); } public static void decrement() { mCountingIdlingResource.decrement(); } public static IdlingResource getIdlingResource() { return mCountingIdlingResource; } }
在 MainActivity 类中声明一个全局变量 mIdlingResource,类型为 CountingIdlingResource,如下所示:
@Nullable private CountingIdlingResource mIdlingResource = null;
编写一个私有方法从 Web 获取水果列表,如下所示:
private ArrayList<String> getFruitList(String data) { ArrayList<String> fruits = new ArrayList<String>(); try { // Get url from async task and set it into a local variable URL url = new URL(data); Log.e("URL", url.toString()); // Create new HTTP connection HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // Set HTTP connection method as "Get" conn.setRequestMethod("GET"); // Do a http request and get the response code int responseCode = conn.getResponseCode(); // check the response code and if success, get response content if (responseCode == HttpURLConnection.HTTP_OK) { BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line; StringBuffer response = new StringBuffer(); while ((line = in.readLine()) != null) { response.append(line); } in.close(); JSONArray jsonArray = new JSONArray(response.toString()); Log.e("HTTPResponse", response.toString()); for(int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); String name = String.valueOf(jsonObject.getString("name")); fruits.add(name); } } else { throw new IOException("Unable to fetch data from url"); } conn.disconnect(); } catch (IOException | JSONException e) { e.printStackTrace(); } return fruits; }
在 onCreate() 方法中创建一个新任务,使用我们的 getFruitList 方法从 Web 获取数据,然后创建一个新适配器并将其设置到列表视图中。一旦我们的工作在线程中完成,也递减空闲资源。代码如下所示:
// Get data class FruitTask implements Runnable { ListView listView; CountingIdlingResource idlingResource; FruitTask(CountingIdlingResource idlingRes, ListView listView) { this.listView = listView; this.idlingResource = idlingRes; } public void run() { //code to do the HTTP request final ArrayList<String> fruitList = getFruitList("http://<your domain or IP>/fruits.json"); try { synchronized (this){ runOnUiThread(new Runnable() { @Override public void run() { // Create adapter and set it to list view final ArrayAdapter adapter = new ArrayAdapter(MainActivity.this, R.layout.item, fruitList); ListView listView = (ListView)findViewById(R.id.listView); listView.setAdapter(adapter); } }); } } catch (Exception e) { e.printStackTrace(); } if (!MyIdlingResource.getIdlingResource().isIdleNow()) { MyIdlingResource.decrement(); // Set app as idle. } } }
此处,水果 URL 被视为 http://<your domain or IP/fruits.json,并格式化为 JSON。内容如下:
[ { "name":"Apple" }, { "name":"Banana" }, { "name":"Cherry" }, { "name":"Dates" }, { "name":"Elderberry" }, { "name":"Fig" }, { "name":"Grapes" }, { "name":"Grapefruit" }, { "name":"Guava" }, { "name":"Jack fruit" }, { "name":"Lemon" }, { "name":"Mango" }, { "name":"Orange" }, { "name":"Papaya" }, { "name":"Pears" }, { "name":"Peaches" }, { "name":"Pineapple" }, { "name":"Plums" }, { "name":"Raspberry" }, { "name":"Strawberry" }, { "name":"Watermelon" } ]
注意 - 将文件放在本地 Web 服务器上并使用它。
现在,查找视图,通过传递 FruitTask 创建一个新线程,递增空闲资源,最后启动任务。
// Find list view ListView listView = (ListView) findViewById(R.id.listView); Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView)); MyIdlingResource.increment(); fruitTask.start();
MainActivity 的完整代码如下所示:
package com.tutorialspoint.espressosamples.myidlingfruitapp; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AppCompatActivity; import androidx.test.espresso.idling.CountingIdlingResource; import android.os.Bundle; import android.util.Log; import android.widget.ArrayAdapter; import android.widget.ListView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; public class MainActivity extends AppCompatActivity { @Nullable private CountingIdlingResource mIdlingResource = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Get data class FruitTask implements Runnable { ListView listView; CountingIdlingResource idlingResource; FruitTask(CountingIdlingResource idlingRes, ListView listView) { this.listView = listView; this.idlingResource = idlingRes; } public void run() { //code to do the HTTP request final ArrayList<String> fruitList = getFruitList( "http://<yourdomain or IP>/fruits.json"); try { synchronized (this){ runOnUiThread(new Runnable() { @Override public void run() { // Create adapter and set it to list view final ArrayAdapter adapter = new ArrayAdapter( MainActivity.this, R.layout.item, fruitList); ListView listView = (ListView) findViewById(R.id.listView); listView.setAdapter(adapter); } }); } } catch (Exception e) { e.printStackTrace(); } if (!MyIdlingResource.getIdlingResource().isIdleNow()) { MyIdlingResource.decrement(); // Set app as idle. } } } // Find list view ListView listView = (ListView) findViewById(R.id.listView); Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView)); MyIdlingResource.increment(); fruitTask.start(); } private ArrayList<String> getFruitList(String data) { ArrayList<String> fruits = new ArrayList<String>(); try { // Get url from async task and set it into a local variable URL url = new URL(data); Log.e("URL", url.toString()); // Create new HTTP connection HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // Set HTTP connection method as "Get" conn.setRequestMethod("GET"); // Do a http request and get the response code int responseCode = conn.getResponseCode(); // check the response code and if success, get response content if (responseCode == HttpURLConnection.HTTP_OK) { BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line; StringBuffer response = new StringBuffer(); while ((line = in.readLine()) != null) { response.append(line); } in.close(); JSONArray jsonArray = new JSONArray(response.toString()); Log.e("HTTPResponse", response.toString()); for(int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); String name = String.valueOf(jsonObject.getString("name")); fruits.add(name); } } else { throw new IOException("Unable to fetch data from url"); } conn.disconnect(); } catch (IOException | JSONException e) { e.printStackTrace(); } return fruits; } }
现在,在应用程序清单文件 AndroidManifest.xml 中添加以下配置
<uses-permission android:name = "android.permission.INTERNET" />
现在,编译上述代码并运行应用程序。My Idling Fruit App 的屏幕截图如下所示:
现在,打开 ExampleInstrumentedTest.java 文件并添加 ActivityTestRule,如下所示:
@Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<MainActivity>(MainActivity.class); Also, make sure the test configuration is done in app/build.gradle dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:rules:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1' androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1" }
添加一个新的测试用例来测试列表视图,如下所示:
@Before public void registerIdlingResource() { IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource()); } @Test public void contentTest() { // click a child item onData(allOf()) .inAdapterView(withId(R.id.listView)) .atPosition(10) .perform(click()); } @After public void unregisterIdlingResource() { IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource()); }
最后,使用 Android Studio 的上下文菜单运行测试用例,并检查所有测试用例是否成功。
Espresso 测试框架 - Intents
Android Intent 用于打开新的活动,无论是内部的(例如从产品列表屏幕打开产品详情屏幕)还是外部的(例如打开拨号器进行拨打电话)。Espresso 测试框架透明地处理内部 Intent 活动,不需要用户进行任何特定操作。但是,调用外部活动确实是一个挑战,因为它超出了我们的范围,即被测试的应用程序。一旦用户调用外部应用程序并离开被测试的应用程序,用户以预定义的动作顺序返回应用程序的可能性就比较小。因此,我们需要在测试应用程序之前假设用户操作。Espresso 提供了两种处理这种情况的选项。它们如下所示:
intended()
这允许用户确保从被测试应用程序中打开了正确的 Intent。
intending()
这允许用户模拟外部活动,例如从相机拍照、从联系人列表拨打电话等,并使用预定义的值集返回应用程序(例如,使用预定义的图像而不是实际图像)。
设置
Espresso 通过插件库支持 Intent 选项,需要在应用程序的 gradle 文件中配置该库。配置选项如下所示:
dependencies { // ... androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1' }
intended()
Espresso Intent 插件提供特殊的匹配器来检查调用的 Intent 是否是预期的 Intent。提供的匹配器及其用途如下所示:
hasAction
这接受 Intent 动作并返回一个匹配器,该匹配器匹配指定的 Intent。
hasData
这接受数据并返回一个匹配器,该匹配器匹配调用时提供给 Intent 的数据。
toPackage
这接受 Intent 包名并返回一个匹配器,该匹配器匹配调用的 Intent 的包名。
现在,让我们创建一个新应用程序并使用 intended()
测试外部活动的应用程序,以了解该概念。
启动 Android Studio。
创建一个新项目,如前所述,并将其命名为 IntentSampleApp。
使用“重构”→“迁移到 AndroidX”选项菜单将应用程序迁移到 AndroidX 框架。
创建一个文本框,一个打开联系人列表的按钮,另一个用于拨打电话的按钮,方法是更改 activity_main.xml,如下所示:
<?xml version = "1.0" encoding = "utf-8"?> <RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:app = "http://schemas.android.com/apk/res-auto" xmlns:tools = "http://schemas.android.com/tools" android:layout_width = "match_parent" android:layout_height = "match_parent" tools:context = ".MainActivity"> <EditText android:id = "@+id/edit_text_phone_number" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerHorizontal = "true" android:text = "" android:autofillHints = "@string/phone_number"/> <Button android:id = "@+id/call_contact_button" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerHorizontal = "true" android:layout_below = "@id/edit_text_phone_number" android:text = "@string/call_contact"/> <Button android:id = "@+id/button" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerHorizontal = "true" android:layout_below = "@id/call_contact_button" android:text = "@string/call"/> </RelativeLayout>
还在 strings.xml 资源文件中添加以下项目:
<string name = "phone_number">Phone number</string> <string name = "call">Call</string> <string name = "call_contact">Select from contact list</string>
现在,在 onCreate 方法下的主活动 (MainActivity.java) 中添加以下代码。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { // ... code // Find call from contact button Button contactButton = (Button) findViewById(R.id.call_contact_button); contactButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // Uri uri = Uri.parse("content://contacts"); Intent contactIntent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); contactIntent.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE); startActivityForResult(contactIntent, REQUEST_CODE); } }); // Find edit view final EditText phoneNumberEditView = (EditText) findViewById(R.id.edit_text_phone_number); // Find call button Button button = (Button) findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(phoneNumberEditView.getText() != null) { Uri number = Uri.parse("tel:" + phoneNumberEditView.getText()); Intent callIntent = new Intent(Intent.ACTION_DIAL, number); startActivity(callIntent); } } }); } // ... code }
在这里,我们为 id 为 call_contact_button 的按钮编写了程序,以打开联系人列表,并为 id 为 button 的按钮编写了程序,以拨打电话。
在 MainActivity 类中添加一个静态变量 REQUEST_CODE,如下所示:
public class MainActivity extends AppCompatActivity { // ... private static final int REQUEST_CODE = 1; // ... }
现在,在 MainActivity 类中添加 onActivityResult 方法,如下所示:
public class MainActivity extends AppCompatActivity { // ... @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE) { if (resultCode == RESULT_OK) { // Bundle extras = data.getExtras(); // String phoneNumber = extras.get("data").toString(); Uri uri = data.getData(); Log.e("ACT_RES", uri.toString()); String[] projection = { ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME }; Cursor cursor = getContentResolver().query(uri, projection, null, null, null); cursor.moveToFirst(); int numberColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); String number = cursor.getString(numberColumnIndex); int nameColumnIndex = cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME); String name = cursor.getString(nameColumnIndex); Log.d("MAIN_ACTIVITY", "Selected number : " + number +" , name : "+name); // Find edit view final EditText phoneNumberEditView = (EditText) findViewById(R.id.edit_text_phone_number); phoneNumberEditView.setText(number); } } }; // ... }
在这里,当用户使用 call_contact_button 按钮打开联系人列表并选择联系人后返回应用程序时,将调用 onActivityResult。一旦调用 onActivityResult 方法,它将获取用户选择的联系人,查找联系电话号码并将其设置为文本框。
运行应用程序并确保一切正常。Intent 示例应用程序的最终外观如下所示:
现在,在应用程序的 gradle 文件中配置 Espresso Intent,如下所示:
dependencies { // ... androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1' }
单击 Android Studio 提供的“立即同步”菜单选项。这将下载 Intent 测试库并正确配置它。
打开 ExampleInstrumentedTest.java 文件,并添加 IntentsTestRule 来代替通常使用的 AndroidTestRule。IntentTestRule 是一个处理 Intent 测试的特殊规则。
public class ExampleInstrumentedTest { // ... code @Rule public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(MainActivity.class); // ... code }
添加两个局部变量来设置测试电话号码和拨号程序包名称,如下所示:
public class ExampleInstrumentedTest { // ... code private static final String PHONE_NUMBER = "1 234-567-890"; private static final String DIALER_PACKAGE_NAME = "com.google.android.dialer"; // ... code }
使用 Android Studio 提供的 Alt + Enter 选项修复导入问题,或者包含以下导入语句:
import android.content.Context; import android.content.Intent; import androidx.test.InstrumentationRegistry; import androidx.test.espresso.intent.rule.IntentsTestRule; 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.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.intent.Intents.intended; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData; import static androidx.test.espresso.intent.matcher.IntentMatchers.toPackage; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static org.hamcrest.core.AllOf.allOf; import static org.junit.Assert.*;
添加以下测试用例以测试拨号器是否已正确调用:
public class ExampleInstrumentedTest { // ... code @Test public void validateIntentTest() { onView(withId(R.id.edit_text_phone_number)) .perform(typeText(PHONE_NUMBER), closeSoftKeyboard()); onView(withId(R.id.button)) .perform(click()); intended(allOf( hasAction(Intent.ACTION_DIAL), hasData("tel:" + PHONE_NUMBER), toPackage(DIALER_PACKAGE_NAME))); } // ... code }
在这里,hasAction、hasData 和 toPackage 匹配器与 allOf 匹配器一起使用,只有在所有匹配器都通过时才成功。
现在,通过 Android Studio 中的上下文菜单运行 ExampleInstrumentedTest。
intending()
Espresso 提供了一个特殊方法 - intending()
来模拟外部 Intent 操作。intending()
接受要模拟的 Intent 的包名,并提供一个方法 respondWith
来设置如何使用模拟的 Intent 进行响应,如下所示:
intending(toPackage("com.android.contacts")).respondWith(result);
在这里,respondWith()
接受类型为 Instrumentation.ActivityResult
的 Intent 结果。我们可以创建新的存根 Intent 并手动设置结果,如下所示:
// Stub intent Intent intent = new Intent(); intent.setData(Uri.parse("content://com.android.contacts/data/1")); Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, intent);
测试是否正确打开了联系人应用程序的完整代码如下所示:
@Test public void stubIntentTest() { // Stub intent Intent intent = new Intent(); intent.setData(Uri.parse("content://com.android.contacts/data/1")); Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, intent); intending(toPackage("com.android.contacts")).respondWith(result); // find the button and perform click action onView(withId(R.id.call_contact_button)).perform(click()); // get context Context targetContext2 = InstrumentationRegistry.getInstrumentation().getTargetContext(); // get phone number String[] projection = { ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME }; Cursor cursor = targetContext2.getContentResolver().query(Uri.parse("content://com.android.cont acts/data/1"), projection, null, null, null); cursor.moveToFirst(); int numberColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); String number = cursor.getString(numberColumnIndex); // now, check the data onView(withId(R.id.edit_text_phone_number)) .check(matches(withText(number))); }
在这里,我们创建了一个新的意图,并将返回值(调用意图时)设置为联系人列表的第一个条目,content://com.android.contacts/data/1。然后,我们设置了intending方法来模拟新创建的意图以代替联系人列表。当调用包com.android.contacts时,它设置并调用我们新创建的意图,并返回列表的默认第一个条目。然后,我们触发了click()操作来启动模拟意图,最后检查调用模拟意图的电话号码与联系人列表中第一个条目的号码是否相同。
如果存在任何缺少导入的问题,则使用Android Studio提供的Alt + Enter选项修复这些导入问题,或者包含以下导入语句:
import android.app.Activity; import android.app.Instrumentation; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; import androidx.test.InstrumentationRegistry; import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.intent.rule.IntentsTestRule; 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.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.intent.Intents.intended; import static androidx.test.espresso.intent.Intents.intending; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData; import static androidx.test.espresso.intent.matcher.IntentMatchers.toPackage; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.core.AllOf.allOf; import static org.junit.Assert.*;
在测试类中添加以下规则,以提供读取联系人列表的权限:
@Rule public GrantPermissionRule permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS);
在应用程序清单文件AndroidManifest.xml中添加以下选项:
<uses-permission android:name = "android.permission.READ_CONTACTS" />
现在,确保联系人列表至少有一个条目,然后使用Android Studio的上下文菜单运行测试。
多个应用程序的UI
Android支持涉及多个应用程序的用户界面测试。让我们假设我们的应用程序有一个选项可以从我们的应用程序移动到消息应用程序以发送消息,然后返回到我们的应用程序。在这种情况下,UI自动化测试框架可以帮助我们测试应用程序。UI自动化可以被认为是Espresso测试框架的良好补充。在选择UI自动化之前,我们可以利用Espresso测试框架中的intending()选项。
设置说明
Android 提供 UI 自动化作为单独的插件。需要按照以下说明在app/build.gradle中进行配置:
dependencies { ... androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' }
编写测试用例的工作流程
让我们了解如何编写基于UI Automator的测试用例:
通过调用getInstance()方法并传递Instrumentation对象来获取UiDevice对象。
myDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); myDevice.pressHome();
使用findObject()方法获取UiObject对象。在使用此方法之前,我们可以打开uiautomatorviewer应用程序来检查目标应用程序的UI组件,因为了解目标应用程序使我们能够编写更好的测试用例。
UiObject button = myDevice.findObject(new UiSelector() .text("Run") .className("android.widget.Button"));
通过调用UiObject的方法来模拟用户交互。例如,使用setText()编辑文本字段,使用click()触发按钮的点击事件。
if(button.exists() && button.isEnabled()) { button.click(); }
最后,我们检查UI是否反映预期状态。
Espresso 测试框架 - 测试录制器
编写测试用例是一项繁琐的工作。即使Espresso提供了非常简单灵活的API,编写测试用例也可能是一项费力且耗时的任务。为了克服这个问题,Android Studio提供了一个录制和生成Espresso测试用例的功能。“录制Espresso测试”位于“运行”菜单下。
让我们按照以下步骤在我们的HelloWorldApp中录制一个简单的测试用例:
打开Android Studio以及HelloWorldApp应用程序。
点击运行→录制Espresso测试并选择MainActivity。
录制器屏幕截图如下:
点击添加断言。它将打开如下所示的应用程序屏幕:
点击Hello World!。录制器屏幕转到选择文本视图如下所示:
再次点击保存断言,这将保存断言并显示如下:
点击确定。它将打开一个新窗口并询问测试用例的名称。默认名称为MainActivityTest
如有必要,更改测试用例名称。
再次点击确定。这将生成一个包含我们录制测试用例的文件MainActivityTest。完整的代码如下:
package com.tutorialspoint.espressosamples.helloworldapp; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import androidx.test.espresso.ViewInteraction; import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.allOf; @LargeTest @RunWith(AndroidJUnit4.class) public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class); @Test public void mainActivityTest() { ViewInteraction textView = onView( allOf(withId(R.id.textView_hello), withText("Hello World!"), childAtPosition(childAtPosition(withId(android.R.id.content), 0),0),isDisplayed())); textView.check(matches(withText("Hello World!"))); } private static Matcher<View> childAtPosition( final Matcher<View> parentMatcher, final int position) { return new TypeSafeMatcher<View>() { @Override public void describeTo(Description description) { description.appendText("Child at position " + position + " in parent "); parentMatcher.describeTo(description); } @Override public boolean matchesSafely(View view) { ViewParent parent = view.getParent(); return parent instanceof ViewGroup && parentMatcher.matches(parent)&& view.equals(((ViewGroup) parent).getChildAt(position)); } }; } }
最后,使用上下文菜单运行测试并检查测试用例是否运行。
Espresso 测试框架 - UI 性能
积极的用户体验在应用程序的成功中起着非常重要的作用。用户体验不仅包括漂亮的用户界面,还包括这些漂亮的用户界面的渲染速度以及每秒帧数。用户界面需要以每秒60帧的速度持续运行才能提供良好的用户体验。
让我们在本节中学习Android中一些用于分析UI性能的选项。
dumpsys
dumpsys是Android设备中内置的工具。它输出有关系统服务的当前信息。dumpsys可以选择转储特定类别的信息。传递gfxinfo将提供所提供包的动画信息。命令如下:
> adb shell dumpsys gfxinfo <PACKAGE_NAME>
framestats
framestats是dumpsys命令的一个选项。一旦使用framestats调用dumpsys,它将转储最近帧的详细帧计时信息。命令如下:
> adb shell dumpsys gfxinfo <PACKAGE_NAME> framestats
它以CSV(逗号分隔值)格式输出信息。CSV格式的输出有助于轻松地将数据推送到Excel中,并随后通过Excel公式和图表提取有用的信息。
systrace
systrace也是Android设备中内置的工具。它捕获并显示应用程序进程的执行时间。可以使用Android Studio终端中的以下命令运行systrace:
python %ANDROID_HOME%/platform-tools/systrace/systrace.py --time=10 -o my_trace_output.html gfx view res
Espresso 测试框架 - 可访问性
可访问性功能是任何应用程序的关键功能之一。供应商开发的应用程序应支持Android SDK设置的最低可访问性指南,才能成为一个成功且有用的应用程序。遵循可访问性标准非常重要,但这并非易事。Android SDK通过提供设计良好的视图来创建可访问的用户界面,提供了极大的支持。
同样,Espresso测试框架通过将可访问性测试功能透明地支持到核心测试引擎中,为开发人员和最终用户提供了极大的帮助。
在Espresso中,开发人员可以通过AccessibilityChecks类启用和配置可访问性测试。示例代码如下:
AccessibilityChecks.enable();
默认情况下,当您执行任何视图操作时,都会运行可访问性检查。检查包括执行操作的视图以及所有后代视图。您可以使用以下代码检查屏幕的整个视图层次结构:
AccessibilityChecks.enable().setRunChecksFromRootView(true);
结论
Espresso 是一个非常棒的工具,Android 开发人员可以使用它以非常简单的方式完全测试他们的应用程序,而无需付出测试框架通常需要的额外努力。它甚至具有录制器,可以创建测试用例而无需手动编写代码。此外,它支持所有类型用户界面测试。通过使用Espresso测试框架,Android开发人员可以自信地开发一个外观精美且成功的应用程序,而无需在短时间内出现任何问题。