微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

android jetpack 导航仪器测试在返回导航时失败

如何解决android jetpack 导航仪器测试在返回导航时失败

我使用 jetpack Navigation 组件 (androidx.navigation) 创建了一个简单的两片段示例应用。第一个片段导航到第二个片段,它使用 OnBackpresseddispatcher 覆盖后退按钮行为。

活动布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/Box_inset_layout_padding"
    tools:context=".navigationcontroller.NavigationControllerActivity">

    <fragment
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:id="@+id/nav_host"
        android:layout_width="match_parent"
        android:layout_height="match_parent"

        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />
</LinearLayout>

片段A:

class FragmentA : Fragment() {

    lateinit var buttonNavigation: Button

    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_a,container,false)
        buttonNavigation = view.findViewById<Button>(R.id.button_navigation)
        buttonNavigation.setonClickListener { Navigation.findNavController(requireActivity(),R.id.nav_host).navigate(R.id.fragmentB) }
        return view
    }
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".navigationcontroller.FragmentA">
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="fragment A" />

    <Button
        android:id="@+id/button_navigation"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="go to B" />
</LinearLayout>

片段B:

class FragmentB : Fragment() {

    override fun onCreateView(inflater: LayoutInflater,savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_b,false)
        requireActivity().onBackpresseddispatcher.addCallback(object : OnBackpressedCallback(true) {
            override fun handleOnBackpressed() {
                val textView = view.findViewById<TextView>(R.id.textView)
                textView.setText("backbutton pressed,press again to go back")
                this.isEnabled = false
            }
        })
        return view
    }
}

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".navigationcontroller.FragmentA">
    
    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="fragment B" />
</FrameLayout>

当我手动测试应用程序时,FragmentB 中后退按钮的预期行为(第一次触摸在没有导航的情况下更改文本,第二次向后导航)工作正常。 我添加了仪器测试来检查 FragmentB 中的后退按钮行为,这就是问题开始出现的地方:

class NavigationControllerActivityTest {

    lateinit var fragmentScenario: FragmentScenario<FragmentB>
    lateinit var navController: TestNavHostController

    @Before
    fun setUp() {
        navController = TestNavHostController(ApplicationProvider.getApplicationContext())

        fragmentScenario = FragmentScenario.launchInContainer(FragmentB::class.java)
        fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
            override fun perform(fragment: FragmentB) {
                Navigation.setViewNavController(fragment.requireView(),navController)
                navController.setLifecycleOwner(fragment.viewLifecycleOwner)
                navController.setonBackpresseddispatcher(fragment.requireActivity().getonBackpresseddispatcher())
                navController.setGraph(R.navigation.nav_graph)
                // simulate backstack from prevIoUs navigation
                navController.navigate(R.id.fragmentA)
                navController.navigate(R.id.fragmentB)
            }
        })
    }

    @Test
    fun whenButtonClickedOnce_TextChangednoNavigation() {
        Espresso.pressBack()
        onView(withId(R.id.textView)).check(matches(withText("backbutton pressed,press again to go back")))
        assertEquals(R.id.fragmentB,navController.currentDestination?.id)
    }

    @Test
    fun whenButtonClickedTwice_NavigationHappens() {
        Espresso.pressBack()
        Espresso.pressBack()
        assertEquals(R.id.fragmentA,navController.currentDestination?.id)
    }
}

不幸的是,当 whenButtonClickedTwice_NavigationHappens 通过时,whenButtonClickedOnce_TextChangednoNavigation 由于文本未更改而失败,就像从未调用 OnBackpressedCallback 一样。由于应用程序在手动测试期间运行良好,因此测试代码肯定有问题。有人可以帮我吗?

解决方法

FragmentB 的 OnBackPressedCallback 被忽略的原因是 OnBackPressedDispatcher 对待它的 OnBackPressedCallback 的方式。它们作为命令链运行,这意味着最近注册的启用的事件将“吃掉”事件,因此其他人不会收到它。因此,最近在 FragmentScenario.onFragment() 内注册的回调(由 lifecycleOwner 启用,因此每当 Fragment 至少处于生命周期 STARTED 状态时。由于在测试期间按下后退按钮时 Fragment 是可见的,回调始终在当时启用),将优先于之前在 FragmentB.onCreateView() 中注册的回调。 因此,必须在执行 TestNavHostController 之前添加 FragmentB.onCreateView() 的回调。

这会导致@Before方法的测试代码发生变化:

@Before
fun setUp() {
    navController = TestNavHostController(ApplicationProvider.getApplicationContext())

    fragmentScenario = FragmentScenario.launchInContainer(FragmentB::class.java,initialState = Lifecycle.State.CREATED)
    fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
        override fun perform(fragment: FragmentB) {
            navController.setLifecycleOwner(fragment.requireActivity())
            navController.setOnBackPressedDispatcher(fragment.requireActivity().getOnBackPressedDispatcher())
            navController.setGraph(R.navigation.nav_graph)
            // simulate backstack from previous navigation
            navController.navigate(R.id.fragmentA)
            navController.navigate(R.id.fragmentB)
        }
    })
    fragmentScenario.moveToState(Lifecycle.State.RESUMED)
    fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
        override fun perform(fragment: FragmentB) {
            Navigation.setViewNavController(fragment.requireView(),navController)
        }
    })
}

最重要的变化是在 CREATED 状态(而不是默认的 RESUMED)下启动 Fragment,以便能够在 onCreateView() 之前修改它。

另外,请注意 Navigation.setViewNavController() 在将片段移动到 onFragment() 状态后在单独的 RESUMED 中运行 - 它接受 View 参数,因此不能在 {{1} 之前使用}}

,

如果您要测试您的 OnBackPressedCallback 逻辑,最好直接执行此操作,而不是尝试测试 Navigation 与默认 Activity 的 OnBackPressedDispatcher 之间的交互。

这意味着您希望通过注入 OnBackPressedDispatcher 来打破 Activity 的 requireActivity().onBackPressedDispatcher (OnBackPressedDispatcher) 和 Fragment 之间的硬依赖关系,从而允许您提供测试特定实例:

class FragmentB(val onBackPressedDispatcher: OnBackPressedDispatcher) : Fragment() {

    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_b,container,false)
        onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                val textView = view.findViewById<TextView>(R.id.textView)
                textView.setText("backbutton pressed,press again to go back")
                this.isEnabled = false
            }
        })
        return view
    }
}

这允许您拥有生产代码 provide a FragmentFactory

class MyFragmentFactory(val activity: FragmentActivity) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader,className: String): Fragment =
        when (loadFragmentClass(classLoader,className)) {
            FragmentB::class.java -> FragmentB(activity.onBackPressedDispatcher)
            else -> super.instantiate(classLoader,className)
        }
}

// Your activity would use this via:
override fun onCreate(savedInstanceState: Bundle?) {
    supportFragmentManager.fragmentFactory = MyFragmentFactory(this)
    super.onCreate(savedInstanceState)
    // ...
}

这意味着您可以编写如下测试:

class NavigationControllerActivityTest {

    lateinit var fragmentScenario: FragmentScenario<FragmentB>
    lateinit var onBackPressedDispatcher: OnBackPressedDispatcher
    lateinit var navController: TestNavHostController

    @Before
    fun setUp() {
        navController = TestNavHostController(ApplicationProvider.getApplicationContext())

        // Create a test specific OnBackPressedDispatcher,// giving you complete control over its behavior
        onBackPressedDispatcher = OnBackPressedDispatcher()

        // Here we use the launchInContainer method that
        // generates a FragmentFactory from a constructor,// automatically figuring out what class you want
        fragmentScenario = launchFragmentInContainer {
            FragmentB(onBackPressedDispatcher)
        }
        fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
            override fun perform(fragment: FragmentB) {
                Navigation.setViewNavController(fragment.requireView(),navController)
                navController.setGraph(R.navigation.nav_graph)
                // Set the current destination to fragmentB
                navController.setCurrentDestination(R.id.fragmentB)
            }
        })
    }

    @Test
    fun whenButtonClickedOnce_FragmentInterceptsBack() {
        // Assert that your FragmentB has already an enabled OnBackPressedCallback
        assertTrue(onBackPressedDispatcher.hasEnabledCallbacks())

        // Now trigger the OnBackPressedDispatcher
        onBackPressedDispatcher.onBackPressed()
        onView(withId(R.id.textView)).check(matches(withText("backbutton pressed,press again to go back")))

        // Check that FragmentB has disabled its Callback
        // ensuring that the onBackPressed() will do the default behavior
        assertFalse(onBackPressedDispatcher.hasEnabledCallbacks())
    }
}

这避免了测试 Navigation 的代码,而是专注于测试您的代码,特别是您与 OnBackPressedDispatcher 的交互。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。