如何在仪器测试中启动带有安全参数的片段?

How can I launch a fragment with safe args in an instrumentation test?

下面我有一个测试 class 旨在单独启动片段并测试 navController 的导航能力。

第一次测试,landingToGameFragmentTest()完美!

第二个测试启动一个片段,该片段依赖于传递给它的安全参数。除此之外,我认为它们的执行方式没有区别。

// Declare navController at top level so it can be accessed from any test in the class
private lateinit var navController: TestNavHostController

// Use Generic type with fragment as upper bound to pass any type of FragmentScenario
private fun <T : Fragment> init(scenario: FragmentScenario<T>) {

    // Create a test navController
    navController = TestNavHostController(
        ApplicationProvider.getApplicationContext()
    )

    scenario.onFragment { fragment ->
        // Link navController to its graph
        navController.setGraph(R.navigation.nav_graph)
        // Link fragment to its navController
        Navigation.setViewNavController(fragment.requireView(), navController)
    }
}

@Test
fun landingToGameFragmentTest() {

    init(launchFragmentInContainer<LandingFragment>(themeResId = THEME))

    // Click button to navigate to GameFragment
    onView(withId(R.id.button_start_game))
        .perform(click())

    assertEquals("Navigation to GameFragment failed",
        R.id.gameFragment,
        navController.currentDestination?.id)
}

@Test
fun gameToLandingFragmentTest() {

    init(launchFragmentInContainer<GameFragment>(themeResId = THEME, fragmentArgs = Bundle()))

    onView(withId(R.id.button_end_game))
        .perform(click())

    assertEquals("Navigation to LandingFragment failed",
        R.id.landingFragment,
        navController.currentDestination?.id)
}

我为其参数设置了一个默认值,但在我向它传递一个空包之前,我仍然遇到空参数异常。现在该片段将启动,但它似乎无法导航到任何其他片段!

我在 SO 上找不到类似的问题,堆栈输出超出了我的范围。

init(launchFragmentInContainer()) 行之后,我单步执行了代码,发现它抛出一个 illegalArgumentException:

public static int parseInt(@RecentlyNonNull String s, int radix) throws NumberFormatException {
    throw new RuntimeException("Stub!");
}

然后导致 getNavigator(),它传递名称“片段”。 然而,唯一的导航器是“导航”和“测试”,我认为它应该是测试。 然后抛出 illegalStateException

/**
 * Retrieves a registered [Navigator] by name.
 *
 * @param name name of the navigator to return
 * @return the registered navigator with the given name
 *
 * @throws IllegalStateException if the Navigator has not been added
 *
 * @see NavigatorProvider.addNavigator
 */
@Suppress("UNCHECKED_CAST")
@CallSuper
public open fun <T : Navigator<*>> getNavigator(name: String): T {
    require(validateName(name)) { "navigator name cannot be an empty string" }
    val navigator = _navigators[name]
        ?: throw IllegalStateException(
            "Could not find Navigator with name \"$name\". You must call " +
                "NavController.addNavigator() for each navigation type."
        )
    return navigator as T
}

最后在 onView(withId(R.id.button_end_game)) 上调用 navigate() 生成:

我可能不必在这个特定实例中保留此测试。但是,我将来肯定需要知道如何单独启动片段(这取决于安全参数)。

感谢您的考虑!!

这里有两个完全不同的问题:

您的 Fragment 需要传递参数。

参数通过 launchFragmentInContainerfragmentArgs 参数传递给您的片段,如 Fragment testing guide.

中所述

每个参数 class,例如您的 LandingFragmentArgs 都有一个构造函数,可以让您直接构造 Args class。然后,您可以使用 toBundle() 方法生成传递给 launchFragmentInContainer:

Bundle
val args = GameFragmentArgs(/* pass in your required args here */)
val bundle = args.toBundle()
init(launchFragmentInContainer<GameFragment>(fragmentArgs = bundle, themeResId = THEME))

您的 NavController 需要将其状态设置为 GameFragment 目的地

您的 TestNavHostController 不知道您的测试需要从与 GameFragment 关联的目的地开始 - 默认情况下,它只会在图表的 startDestination 上(您尝试触发的任何操作都不存在)。

根据 Test Navigation documentation:

TestNavHostController provides a setCurrentDestination method that allows you to set the current destination so that the NavController is in the correct state before your test begins.

因此您需要确保在 init 调用之后调用 setCurrentDestination

val args = GameFragmentArgs(/* pass in your required args here */)
val bundle = args.toBundle()
val scenario = launchFragmentInContainer<GameFragment>(fragmentArgs = bundle, themeResId = THEME)
init(scenario)

// Ensure that the NavController is set to the expected destination
// using the ID from your navigation graph associated with GameFragment
scenario.onFragment {
  // Just like setGraph(), this needs to be called on the main thread
  navController.setCurrentDestination(R.id.game_fragment, bundle)
}