测试单个应用内的用户交互有助于确保用户在与应用交互时不会遇到意外结果或体验不佳的情况。如果您需要验证应用的界面是否正常运行,应养成创建界面测试的习惯。
由 AndroidX Test 提供的 Espresso 测试框架提供了一些 API,用于编写界面测试以模拟单个目标应用内的用户交互。Espresso 测试可以在搭载 Android 2.3.3(API 级别 10)及更高版本的设备上运行。使用 Espresso 的主要好处在于,它可以自动同步测试操作与您正在测试的应用的界面。Espresso 会检测主线程何时处于空闲状态,以便可以在适当的时间运行测试命令,从而提高测试的可靠性。此外,借助该功能,您不必在测试代码中添加任何计时解决方法,如 Thread.sleep()
。
Espresso 测试框架是基于插桩的 API,可与 AndroidJUnitRunner
测试运行程序一起使用。
设置 Espresso
在使用 Espresso 构建界面测试之前,请务必设置对 Espresso 库的依赖项引用:
dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' }
在测试设备上关闭动画 - 如果让系统动画在测试设备上保持开启状态,可能会导致意外结果或导致测试失败。通过以下方式关闭动画:在“设置”中打开“开发者选项”,然后关闭以下所有选项:
- 窗口动画缩放
- 过渡动画缩放
- Animator 时长缩放
如果您希望设置项目以使用除核心 API 所提供功能之外的 Espresso 功能,请参阅 Espresso 专用指南。
创建 Espresso 测试类
如需创建 Espresso 测试,请遵循以下编程模型:
-
通过调用
onView()
方法或AdapterView
控件的onData()
方法,在Activity
中找到要测试的界面组件(例如,应用中的登录按钮)。 -
通过调用
ViewInteraction.perform()
或DataInteraction.perform()
方法并传入用户操作(例如,点击登录按钮),模拟要在该界面组件上执行的特定用户交互。如需对同一界面组件上的多项操作进行排序,请在方法参数中使用逗号分隔列表将它们链接起来。 - 根据需要重复上述步骤,以模拟目标应用中跨多个 Activity 的用户流。
-
执行这些用户交互后,使用
ViewAssertions
方法检查界面是否反映了预期的状态或行为。
下面几部分更详细地介绍了这些步骤。
以下代码段展示了测试类如何调用此基本工作流程:
Kotlin
onView(withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher .perform(click()) // click() is a ViewAction .check(matches(isDisplayed())) // matches(isDisplayed()) is a ViewAssertion
Java
onView(withId(R.id.my_view)) // withId(R.id.my_view) is a ViewMatcher .perform(click()) // click() is a ViewAction .check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
将 Espresso 与 ActivityTestRule 一起使用
下文介绍如何创建新的 JUnit 4 型 Espresso 测试,并使用 ActivityTestRule
减少您需要编写的样板代码量。通过使用 ActivityTestRule
,测试框架会在带有 @Test
注释的每个测试方法运行之前以及带有 @Before
注释的所有方法运行之前启动被测 Activity。该框架将在测试完成并且带有 @After
注释的所有方法都运行后关闭该 Activity。
Kotlin
package com.example.android.testing.espresso.BasicSample import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import androidx.test.rule.ActivityTestRule import androidx.test.runner.AndroidJUnit4 @RunWith(AndroidJUnit4::class) @LargeTest class ChangeTextBehaviorTest { private lateinit var stringToBetyped: String @get:Rule var activityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) @Before fun initValidString() { // Specify a valid string. stringToBetyped = "Espresso" } @Test fun changeText_sameActivity() { // Type text and then press the button. onView(withId(R.id.editTextUserInput)) .perform(typeText(stringToBetyped), closeSoftKeyboard()) onView(withId(R.id.changeTextBt)).perform(click()) // Check that the text was changed. onView(withId(R.id.textToBeChanged)) .check(matches(withText(stringToBetyped))) } }
Java
package com.example.android.testing.espresso.BasicSample; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; @RunWith(AndroidJUnit4.class) @LargeTest public class ChangeTextBehaviorTest { private String stringToBetyped; @Rule public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(MainActivity.class); @Before public void initValidString() { // Specify a valid string. stringToBetyped = "Espresso"; } @Test public void changeText_sameActivity() { // Type text and then press the button. onView(withId(R.id.editTextUserInput)) .perform(typeText(stringToBetyped), closeSoftKeyboard()); onView(withId(R.id.changeTextBt)).perform(click()); // Check that the text was changed. onView(withId(R.id.textToBeChanged)) .check(matches(withText(stringToBetyped))); } }
访问界面组件
您必须先指定界面组件或视图,然后 Espresso 才能与被测应用进行交互。Espresso 支持使用 Hamcrest 匹配器指定应用中的视图和适配器。
如需查看视图,请调用 onView()
方法并传入用于指定目标视图的视图匹配器。指定视图匹配器部分对此进行了更详细的说明。onView()
方法将返回一个 ViewInteraction
对象,该对象允许测试与视图进行交互。但是,如果希望在 RecyclerView
布局中查找视图,调用 onView()
方法可能不起作用。在这种情况下,请按照在 AdapterView 中查找视图中的说明进行操作。
注意:onView()
方法不检查您指定的视图是否有效。相反,Espresso 根据提供的匹配器仅搜索当前视图层次结构。如果未找到匹配项,该方法会抛出 NoMatchingViewException
。
以下代码段展示了如何编写一个先访问 EditText
字段,再输入文本字符串,接着关闭虚拟键盘,然后执行按钮点击操作的测试。
Kotlin
fun testChangeText_sameActivity() { // Type text and then press the button. onView(withId(R.id.editTextUserInput)) .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard()) onView(withId(R.id.changeTextButton)).perform(click()) // Check that the text was changed. ... }
Java
public void testChangeText_sameActivity() { // Type text and then press the button. onView(withId(R.id.editTextUserInput)) .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard()); onView(withId(R.id.changeTextButton)).perform(click()); // Check that the text was changed. ... }
指定视图匹配器
您可以使用以下方法指定视图匹配器:
- 调用
ViewMatchers
类中的方法。例如,如需通过查找视图显示的文本字符串查找视图,您可以调用以下方法:Kotlin
onView(withText("Sign-in"))
Java
onView(withText("Sign-in"));
同样,您可以调用
withId()
并提供视图的资源 ID (R.id
),如以下示例所示:Kotlin
onView(withId(R.id.button_signin))
Java
onView(withId(R.id.button_signin));
不能保证 Android 资源 ID 是唯一的。如果测试尝试匹配由多个视图使用的某个资源 ID,Espresso 会抛出
AmbiguousViewMatcherException
。 -
使用
Matchers
类。您可以使用allOf()
方法组合多个匹配器,例如containsString()
和instanceOf()
。此方法可让您更精细地过滤匹配结果,如以下示例所示:Kotlin
onView(allOf(withId(R.id.button_signin), withText("Sign-in")))
Java
onView(allOf(withId(R.id.button_signin), withText("Sign-in")));
您可以使用
not
关键字过滤与匹配器不对应的视图,如以下示例所示:Kotlin
onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))))
Java
onView(allOf(withId(R.id.button_signin), not(withText("Sign-out"))));
如需在测试中使用这些方法,请导入
org.hamcrest.Matchers
软件包。如需详细了解 Hamcrest 匹配,请访问 Hamcrest 网站。
如需提高 Espresso 测试的性能,请指定查找目标视图所需的最少匹配信息。例如,如果某个视图可通过其描述性文本进行唯一标识,您无需指定该视图也可从 TextView
实例分配。
在 AdapterView 中查找视图
在 AdapterView
微件中,视图会在运行时由子视图动态填充。如果您要测试的目标视图位于 AdapterView
(例如 ListView
、GridView
或 Spinner
)内,则 onView()
方法可能不起作用,因为只能将一部分视图加载到当前视图层次结构中。
应改为调用 onData()
方法获取 DataInteraction
对象,以访问目标视图元素。Espresso 负责将目标视图元素加载到当前视图层次结构中。Espresso 还负责滚动到目标元素,并将该元素置于焦点上。
注意:onData()
方法不检查您指定的项是否与视图对应。Espresso 仅搜索当前视图层次结构。如果未找到匹配项,该方法会抛出 NoMatchingViewException
。
以下代码段展示了如何结合使用 onData()
方法和 Hamcrest 匹配搜索列表中包含给定字符串的特定行。在本例中,LongListActivity
类包含通过 SimpleAdapter
公开的字符串列表。
Kotlin
onData(allOf(`is`(instanceOf(Map::class.java)), hasEntry(equalTo(LongListActivity.ROW_TEXT), `is`("test input"))))
Java
onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo(LongListActivity.ROW_TEXT), is("test input"))));
执行操作
调用 ViewInteraction.perform()
或 DataInteraction.perform()
方法,以模拟界面组件上的用户交互。您必须将一个或多个 ViewAction
对象作为参数传入。Espresso 将按照给定的顺序依次触发每项操作,并在主线程中执行这些操作。
ViewActions
类提供了用于指定常见操作的辅助程序方法的列表。您可以将这些方法用作方便的快捷方式,而不是创建和配置单个 ViewAction
对象。您可以指定以下操作:
-
ViewActions.click()
:点击视图。 -
ViewActions.typeText()
:点击视图并输入指定的字符串。 -
ViewActions.scrollTo()
:滚动到视图。目标视图必须是由ScrollView
派生的子类,并且其android:visibility
属性的值必须为VISIBLE
。对于扩展AdapterView
的视图(例如ListView
),onData()
方法将负责为您滚动。 -
ViewActions.pressKey()
:使用指定的键码执行按键操作。 -
ViewActions.clearText()
:清除目标视图中的文本。
如果目标视图位于 ScrollView
内,请先执行 ViewActions.scrollTo()
操作以在屏幕中显示该视图,然后再继续执行其他操作。如果已显示该视图,则 ViewActions.scrollTo()
操作将不起作用。
使用 Espresso Intent 单独测试 Activity
Espresso Intent 支持对应用发出的 intent 进行验证和打桩。使用 Espresso Intent,您可以通过以下方式单独测试应用、Activity 或服务:拦截传出 intent,对结果进行打桩,然后将其发送回被测组件。
如需开始使用 Espresso Intent 进行测试,您需要将以下代码行添加到应用的 build.gradle 文件中:
dependencies { androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0' }
如需测试 intent,您需要创建 IntentsTestRule 类(与 ActivityTestRule 类非常相似)的实例。IntentsTestRule 类会在每次测试前初始化 Espresso Intent,终止托管 Activity,并在每次测试后释放 Espresso Intent。
以下代码段中显示的测试类提供了显式 intent 的简单测试。它会测试在构建首个应用教程中创建的 Activity 和 intent。
Kotlin
private const val MESSAGE = "This is a test" private const val PACKAGE_NAME = "com.example.myfirstapp" @RunWith(AndroidJUnit4::class) class SimpleIntentTest { /* Instantiate an IntentsTestRule object. */ @get:Rule var intentsRule: IntentsTestRule<MainActivity> = IntentsTestRule(MainActivity::class.java) @Test fun verifyMessageSentToMessageActivity() { // Types a message into a EditText element. onView(withId(R.id.edit_message)) .perform(typeText(MESSAGE), closeSoftKeyboard()) // Clicks a button to send the message to another // activity through an explicit intent. onView(withId(R.id.send_message)).perform(click()) // Verifies that the DisplayMessageActivity received an intent // with the correct package name and message. intended(allOf( hasComponent(hasShortClassName(".DisplayMessageActivity")), toPackage(PACKAGE_NAME), hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE))) } }
Java
@Large @RunWith(AndroidJUnit4.class) public class SimpleIntentTest { private static final String MESSAGE = "This is a test"; private static final String PACKAGE_NAME = "com.example.myfirstapp"; /* Instantiate an IntentsTestRule object. */ @Rule public IntentsTestRule<MainActivity> intentsRule = new IntentsTestRule<>(MainActivity.class); @Test public void verifyMessageSentToMessageActivity() { // Types a message into a EditText element. onView(withId(R.id.edit_message)) .perform(typeText(MESSAGE), closeSoftKeyboard()); // Clicks a button to send the message to another // activity through an explicit intent. onView(withId(R.id.send_message)).perform(click()); // Verifies that the DisplayMessageActivity received an intent // with the correct package name and message. intended(allOf( hasComponent(hasShortClassName(".DisplayMessageActivity")), toPackage(PACKAGE_NAME), hasExtra(MainActivity.EXTRA_MESSAGE, MESSAGE))); } }
如需详细了解 Espresso Intent,请参阅 AndroidX Test 网站上的 Espresso Intent 文档。您还可以下载 IntentsBasicSample 和 IntentsAdvancedSample 代码示例。
使用 Espresso Web 测试 WebView
使用 Espresso Web,您可以测试包含在 Activity 中的 WebView
组件。它使用 WebDriver API 检查和控制 WebView
的行为。
如需开始使用 Espresso Web 进行测试,您需要将以下代码行添加到应用的 build.gradle 文件中:
dependencies { androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0' }
在使用 Espresso Web 创建测试的过程中,当您实例化 ActivityTestRule 对象以测试 Activity 时,需要在 WebView
上启用 JavaScript。在测试中,您可以选择 WebView
中显示的 HTML 元素并模拟用户交互,例如在文本框中输入文本,然后点击某个按钮。完成这些操作后,您可以验证网页上的结果是否与预期结果一致。
以下代码段中,该类测试被测 Activity 中 ID 值为“webview”的 WebView
组件。typeTextInInput_clickButton_SubmitsForm()
测试先选择网页上的一个 <input>
元素,再输入一些文本,然后检查出现在另一个元素中的文本。
Kotlin
private const val MACCHIATO = "Macchiato" private const val DOPPIO = "Doppio" @LargeTest @RunWith(AndroidJUnit4::class) class WebViewActivityTest { @get:Rule val activityRule = object : ActivityTestRule<WebViewActivity>( WebViewActivity::class.java, false, /* Initial touch mode */ false /* launch activity */ ) { override fun afterActivityLaunched() { // Enable JavaScript. onWebView().forceJavascriptEnabled() } } @Test fun typeTextInInput_clickButton_SubmitsForm() { // Lazily launch the Activity with a custom start Intent per test activityRule.launchActivity(withWebFormIntent()) // Selects the WebView in your layout. // If you have multiple WebViews you can also use a // matcher to select a given WebView, onWebView(withId(R.id.web_view)). onWebView() // Find the input element by ID .withElement(findElement(Locator.ID, "text_input")) // Clear previous input .perform(clearElement()) // Enter text into the input element .perform(DriverAtoms.webKeys(MACCHIATO)) // Find the submit button .withElement(findElement(Locator.ID, "submitBtn")) // Simulate a click via JavaScript .perform(webClick()) // Find the response element by ID .withElement(findElement(Locator.ID, "response")) // Verify that the response page contains the entered text .check(webMatches(getText(), containsString(MACCHIATO))) } }
Java
@LargeTest @RunWith(AndroidJUnit4.class) public class WebViewActivityTest { private static final String MACCHIATO = "Macchiato"; private static final String DOPPIO = "Doppio"; @Rule public ActivityTestRule<WebViewActivity> activityRule = new ActivityTestRule<WebViewActivity>(WebViewActivity.class, false /* Initial touch mode */, false /* launch activity */) { @Override protected void afterActivityLaunched() { // Enable JavaScript. onWebView().forceJavascriptEnabled(); } } @Test public void typeTextInInput_clickButton_SubmitsForm() { // Lazily launch the Activity with a custom start Intent per test activityRule.launchActivity(withWebFormIntent()); // Selects the WebView in your layout. // If you have multiple WebViews you can also use a // matcher to select a given WebView, onWebView(withId(R.id.web_view)). onWebView() // Find the input element by ID .withElement(findElement(Locator.ID, "text_input")) // Clear previous input .perform(clearElement()) // Enter text into the input element .perform(DriverAtoms.webKeys(MACCHIATO)) // Find the submit button .withElement(findElement(Locator.ID, "submitBtn")) // Simulate a click via JavaScript .perform(webClick()) // Find the response element by ID .withElement(findElement(Locator.ID, "response")) // Verify that the response page contains the entered text .check(webMatches(getText(), containsString(MACCHIATO))); } }
如需详细了解 Espresso Web,请参阅 AndroidX Test 网站上的 Espresso Web 文档。您还可以将此代码段作为 Espresso Web 代码示例的一部分下载。
验证结果
调用 ViewInteraction.check()
或 DataInteraction.check()
方法以断言界面中的视图与某种预期状态匹配。您必须将 ViewAssertion
对象作为参数传入。如果断言失败,Espresso 会抛出 AssertionFailedError
。
ViewAssertions
类提供了用于指定常见断言的辅助程序方法的列表。可以使用的断言包括:
-
doesNotExist
:断言当前视图层次结构中没有符合指定条件的视图。 -
matches
:断言当前视图层次结构中存在指定的视图,并且其状态与某个给定的 Hamcrst 匹配器匹配。 -
selectedDescendentsMatch
:断言存在父视图的指定子视图,并且其状态与某个给定的 Hamcrst 匹配器匹配。
以下代码段展示了如何检查界面中显示的文本与先前在 EditText
字段中输入的文本是否具有相同的值。
Kotlin
fun testChangeText_sameActivity() { // Type text and then press the button. ... // Check that the text was changed. onView(withId(R.id.textToBeChanged)) .check(matches(withText(STRING_TO_BE_TYPED))) }
Java
public void testChangeText_sameActivity() { // Type text and then press the button. ... // Check that the text was changed. onView(withId(R.id.textToBeChanged)) .check(matches(withText(STRING_TO_BE_TYPED))); }
在设备或模拟器上运行 Espresso 测试
您可以通过 Android Studio 或命令行运行 Espresso 测试。请务必在项目中将 AndroidJUnitRunner
指定为默认插桩测试运行程序。
如需运行 Espresso 测试,请按照测试入门中所述的插桩测试运行步骤进行操作。
您还应阅读 Espresso API 参考文档。
其他资源
如需详细了解如何在 Android 测试中使用 UI Automator,请参阅以下资源。
示例
- Espresso 代码示例包含各种各样的 Espresso 示例。