测试应用的 Fragment

Fragment 在应用中充当可重复使用的容器,使您能够在各种 Activity 和布局配置下呈现相同的界面布局。鉴于这些 Fragment 的通用性,请务必验证它们是否能够提供一致的、具有资源效率的体验:

  • Fragment 在各种布局配置下的外观应一致,包括支持较大屏幕尺寸或横向设备方向的配置。
  • 请勿为 Fragment 创建视图层次结构,除非该 Fragment 对用户可见。

本文档介绍如何在评估每个 Fragment 行为的测试中加入框架提供的 API。

推动 Fragment 的状态

为了帮助设置执行这些测试的条件,AndroidX 提供了一个 FragmentScenario 库,便于您创建 Fragment 并更改其状态。

声明依赖项

为了按预期使用 FragmentScenario,请在应用的测试 APK 中定义 fragment-testing 工件,如以下代码段所示:

app/build.gradle

    dependencies {
        def fragment_version = "1.2.4"
        // ...
        debugImplementation 'androidx.fragment:fragment-testing:$fragment_version'
    }
    

要查看此库的当前版本,请参阅版本页上有关 Fragment 的信息。

创建 Fragment

FragmentScenario 包括用于启动以下类型的 Fragment 的方法:

这些方法还支持以下类型的 Fragment:

  • 包含界面的图形 Fragment。要启动此类 Fragment,请调用 launchFragmentInContainer()FragmentScenario 将 Fragment 附加到 Activity 的根视图控制器。此托管 Activity 原本为空。
  • 非图形 Fragment(有时称为“无头 Fragment”),用于存储若干 Activity 中包含的信息或对这些信息进行短期处理。要启动此类 Fragment,请调用 launchFragment()FragmentScenario 将此类 Fragment 附加到一个完全为空的 Activity,即没有根视图的 Activity。

启动其中一种类型的 Fragment 后,FragmentScenario 将被测 Fragment 推动到 RESUMED 状态。此状态表示 Fragment 正在运行。如果您测试的是图形 Fragment,则它对用户也是可见的,因此您可以使用 Espresso 界面测试评估其界面元素的相关信息。

以下代码段演示了如何启动每种类型的 Fragment:

图形 Fragment 示例

    @RunWith(AndroidJUnit4::class)
    class MyTestSuite {
        @Test fun testEventFragment() {
            // The "fragmentArgs" and "factory" arguments are optional.
            val fragmentArgs = Bundle().apply {
                putInt("selectedListItem", 0)
            }
            val factory = MyFragmentFactory()
            val scenario = launchFragmentInContainer<MyFragment>(
                    fragmentArgs, factory)
            onView(withId(R.id.text)).check(matches(withText("Hello World!")))
        }
    }
    

非图形 Fragment 示例

    @RunWith(AndroidJUnit4::class)
    class MyTestSuite {
        @Test fun testEventFragment() {
            // The "fragmentArgs" and "factory" arguments are optional.
            val fragmentArgs = Bundle().apply {
                putInt("numElements", 0)
            }
            val factory = MyFragmentFactory()
            val scenario = launchFragment<MyFragment>(fragmentArgs, factory)
        }
    }
    

重新创建 Fragment

如果设备资源不足,系统可能会清除包含 Fragment 的 Activity,要求您的应用在用户返回到应用时重新创建该 Fragment。要模拟这种情况,请调用 recreate()

    @RunWith(AndroidJUnit4::class)
    class MyTestSuite {
        @Test fun testEventFragment() {
            val scenario = launchFragmentInContainer<MyFragment>()
            scenario.recreate()
        }
    }
    

FragmentScenario 类重新创建被测 Fragment 时,Fragment 将返回到重新创建前所处的生命周期状态。

将 Fragment 推动到新状态

在应用的界面测试中,通常只需启动并重新创建被测 Fragment 即可。但是,在更精细的单元测试中,您还可以评估 Fragment 从一个生命周期状态转换到另一个生命周期状态时的行为。

要将 Fragment 推动到其他生命周期状态,请调用 moveToState()。此方法支持以下状态作为参数:CREATEDSTARTEDRESUMEDDESTROYED。此操作会模拟包含您的 Fragment 的 Activity 由于被其他应用或系统操作打断而更改其状态的情况。

以下代码段演示了 moveToState() 的示例用法:

    @RunWith(AndroidJUnit4::class)
    class MyTestSuite {
        @Test fun testEventFragment() {
            val scenario = launchFragmentInContainer<MyFragment>()
            scenario.moveToState(State.CREATED)
        }
    }
    

在 Fragment 中触发操作

要在被测 Fragment 中触发操作,请使用 Espresso 视图匹配器与视图中的元素互动:

    @RunWith(AndroidJUnit4::class)
    class MyTestSuite {
        @Test fun testEventFragment() {
            val scenario = launchFragmentInContainer<MyFragment>()
            onView(withId(R.id.refresh))
                    .perform(click())
        }
    }
    

如果您需要对 Fragment 本身调用方法,例如响应选项菜单中的选择,您可以通过实现 FragmentAction 安全地执行此操作:

    @RunWith(AndroidJUnit4::class)
    class MyTestSuite {
        @Test fun testEventFragment() {
            val scenario = launchFragmentInContainer<MyFragment>()
            scenario.onFragment(fragment ->
                fragment.onOptionsItemSelected(clickedItem) {
                    // Update fragment's state based on selected item.
                }
            }
        }
    }
    

测试对话框操作

FragmentScenario 还支持测试对话框。 即使对话框是图形 Fragment 的实例,您也可以使用 launchFragment() 方法,以便在对话框本身中填充该对话框的元素,而不是在启动对话框的 Activity 中填充。

以下代码段可测试对话框关闭过程:

    @RunWith(AndroidJUnit4::class)
    class MyTestSuite {
        @Test fun testDismissDialogFragment() {
            // Assumes that "MyDialogFragment" extends the DialogFragment class.
            with(launchFragment<MyDialogFragment>()) {
                onFragment { fragment ->
                    assertThat(fragment.dialog).isNotNull()
                    assertThat(fragment.requireDialog().isShowing).isTrue()
                    fragment.dismiss()
                    fragment.requireFragmentManager().executePendingTransactions()
                    assertThat(fragment.dialog).isNull()
                }

                // Assumes that the dialog had a button
                // containing the text "Cancel".
                onView(withText("Cancel")).check(doesNotExist())
            }
        }
    }