测试单个应用的界面

测试单个应用内的用户交互有助于确保用户在与应用交互时不会遇到意外结果或体验不佳的情况。如果您需要验证应用的界面是否正常运行,应养成创建界面测试的习惯。

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 测试,请遵循以下编程模型:

  1. 通过调用 onView() 方法或 AdapterView 控件的 onData() 方法,在 Activity 中找到要测试的界面组件(例如,应用中的登录按钮)。
  2. 通过调用 ViewInteraction.perform()DataInteraction.perform()方法并传入用户操作(例如,点击登录按钮),模拟要在该界面组件上执行的特定用户交互。如需对同一界面组件上的多项操作进行排序,请在方法参数中使用逗号分隔列表将它们链接起来。
  3. 根据需要重复上述步骤,以模拟目标应用中跨多个 Activity 的用户流。
  4. 执行这些用户交互后,使用 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(例如 ListViewGridViewSpinner)内,则 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 对象。您可以指定以下操作:

如果目标视图位于 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 文档。您还可以下载 IntentsBasicSampleIntentsAdvancedSample 代码示例。

使用 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,请参阅以下资源。

示例

Codelab