测试 fragment

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

fragment 在应用中充当可重复使用的容器,使您能够在各种 activity 和布局配置下呈现相同的界面布局。鉴于 fragment 的多功能性,请务必验证它们是否提供能够高效利用资源的一致体验。请注意以下几点:

  • fragment 不应依赖于特定的父 activity 或 fragment。
  • 不应创建 fragment 的视图层次结构,除非该 fragment 对用户可见。

为了帮助设置执行这些测试的条件,AndroidX fragment-testing 库提供了 FragmentScenario 类,用于创建 fragment 以及更改其 Lifecycle.State

声明依赖项

如需使用 FragmentScenario,请在fragment-testing-manifest 应用的 build.gradle 文件(使用 debugImplementation)以及使用 androidTestImplementationfragment-testing 工件,如 示例:

Groovy

dependencies {
    def fragment_version = "1.8.5"

    debugImplementation "androidx.fragment:fragment-testing-manifest:$fragment_version"

    androidTestImplementation "androidx.fragment:fragment-testing:$fragment_version"
}

Kotlin

dependencies {
    val fragment_version = "1.8.5"

    debugImplementation("androidx.fragment:fragment-testing-manifest:$fragment_version")

    androidTestImplementation("androidx.fragment:fragment-testing:$fragment_version")
}

本页中的测试示例使用了 EspressoTruth 库中的断言。如需了解其他可用的测试和断言库,请参阅针对 AndroidX Test 设置项目

创建 Fragment

FragmentScenario 包含下面几种用于在测试中启动 fragment 的方法:

  • launchInContainer(),用于测试 fragment 的界面。FragmentScenario 会将 fragment 附加到 activity 的根视图控制器。包含 fragment 的这一 activity 原本为空。
  • launch(),用于在没有 fragment 界面的情况下进行测试。FragmentScenario 会将这种类型的 fragment 附加到一个空 activity,即没有根视图的 activity。

启动其中一种类型的 fragment 后,FragmentScenario 会将被测 fragment 推动到指定状态。默认情况下,此状态为 RESUMED,但您可以使用 initialState 参数替换此状态。RESUMED 状态表示 fragment 正在运行且对用户可见。您可以使用 Espresso 界面测试评估有关其界面元素的信息。

以下代码示例展示了如何使用每种方法启动 fragment:

launchInContainer() 示例

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        // The "fragmentArgs" argument is optional.
        val fragmentArgs = bundleOf(selectedListItem to 0)
        val scenario = launchFragmentInContainer<EventFragment>(fragmentArgs)
        ...
    }
}

launch() 示例

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        // The "fragmentArgs" arguments are optional.
        val fragmentArgs = bundleOf("numElements" to 0)
        val scenario = launchFragment<EventFragment>(fragmentArgs)
        ...
    }
}

提供依赖项

如果 fragment 具有依赖项,您可以通过向 launchInContainer()launch() 方法提供自定义 FragmentFactory 来提供这些依赖项的测试版本。

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        val someDependency = TestDependency()
        launchFragmentInContainer {
            EventFragment(someDependency)
        }
        ...
    }
}

如需详细了解如何使用 FragmentFactory 为 fragment 提供依赖项,请参阅 fragment 管理器

将 Fragment 推动到新状态

在应用的界面测试中,通常只需启动被测 fragment 并从 RESUMED 状态开始对其进行测试即可。不过,在更精细的单元测试中,您还可以评估 fragment 从一种生命周期状态转换为另一种生命周期状态时的行为。您可以通过向任何 launchFragment*() 函数传递 initialState 参数来指定初始状态。

如需将 fragment 推动到其他生命周期状态,请调用 moveToState()。此方法支持以下状态作为参数:CREATEDSTARTEDRESUMEDDESTROYED。此方法会模拟 fragment 或包含 fragment 的 activity 出于任何原因而改变其状态的情况。

以下示例启动了一个处于 INITIALIZED 状态的测试 fragment,然后将其转换为 RESUMED 状态:

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        val scenario = launchFragmentInContainer<EventFragment>(
            initialState = Lifecycle.State.INITIALIZED
        )
        // EventFragment has gone through onAttach(), but not onCreate().
        // Verify the initial state.
        scenario.moveToState(Lifecycle.State.RESUMED)
        // EventFragment moves to CREATED -> STARTED -> RESUMED.
        ...
    }
}

重新创建 Fragment

如果应用在资源不足的设备上运行,则系统可能会销毁包含 fragment 的 activity。在这种情况下,应用需要在用户返回到它时重新创建该 fragment。如需模拟这种情况,请调用 recreate()

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

FragmentScenario.recreate() 会销毁 Fragment 及其宿主,然后重新创建它们。当 FragmentScenario 类重新创建被测 Fragment 时,Fragment 会回到它被销毁前所处的生命周期状态。

与界面 Fragment 交互

如需在被测 fragment 中触发界面操作,请使用 Espresso 视图匹配器与视图中的元素交互:

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        val scenario = launchFragmentInContainer<EventFragment>()
        onView(withId(R.id.refresh)).perform(click())
        // Assert some expected behavior
        ...
    }
}

如果您需要对 fragment 本身调用某种方法(如响应选项菜单中的选择),您可以安全地执行此操作,方法是使用 FragmentScenario.onFragment() 并传入 FragmentAction 来获取对 fragment 的引用:

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        val scenario = launchFragmentInContainer<EventFragment>()
        scenario.onFragment { fragment ->
            fragment.myInstanceMethod()
        }
    }
}

测试对话框操作

FragmentScenario 还支持测试对话框 Fragment。虽然对话框 fragment 具有界面元素,但其布局会填充在单独的窗口中,而不是在 activity 本身中。因此,请使用 FragmentScenario.launch() 测试对话框 Fragment。

以下示例将测试对话框关闭过程:

@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.parentFragmentManager.executePendingTransactions()
                assertThat(fragment.dialog).isNull()
            }
        }

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