android jetpack 导航仪器测试在返回导航上失败
Posted
技术标签:
【中文标题】android jetpack 导航仪器测试在返回导航上失败【英文标题】:android jetpack navigation instrumented test fail on back navigation 【发布时间】:2021-06-10 23:02:01 【问题描述】:我使用 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_
android:layout_
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_
android:layout_
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_
android:layout_
tools:context=".navigationcontroller.FragmentA">
<TextView
android:layout_
android:layout_
android:text="fragment A" />
<Button
android:id="@+id/button_navigation"
android:layout_
android:layout_
android:text="go to B" />
</LinearLayout>
片段B:
class FragmentB : Fragment()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
val view = inflater.inflate(R.layout.fragment_b, container, 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_
android:layout_
tools:context=".navigationcontroller.FragmentA">
<TextView
android:id="@+id/textView"
android:layout_
android:layout_
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
一样。由于应用程序在手动测试期间运行良好,因此测试代码肯定有问题。谁能帮帮我?
【问题讨论】:
【参考方案1】:如果您尝试测试您的 OnBackPressedCallback
逻辑,最好直接执行此操作,而不是尝试测试 Navigation 与默认活动的 OnBackPressedDispatcher
之间的交互。
这意味着您希望通过注入OnBackPressedDispatcher
来打破活动的OnBackPressedDispatcher
(requireActivity().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
的交互。
【讨论】:
您能解释一下调用 FragmentScenario.launchInContainer ... 时使用的 Kotlin 功能吗?它有一个参数 - 尾随 lambda。唯一可以接受单个参数的 launchInContainer 版本是具有默认值的版本,但它需要传递 fragmentClass: ClasslaunchFragmentInContainer
extension method,它采用 crossinline instantiate: () -> F
作为尾随 lambda。【参考方案2】:
FragmentB 的OnBackPressedCallback
被忽略的原因是OnBackPressedDispatcher
如何对待它的OnBackPressedCallback
s。它们作为命令链运行,这意味着最近注册的启用的将“吃掉”该事件,因此其他人将不会收到它。因此,最近在FragmentScenario.onFragment()
中注册了回调(由lifecycleOwner
启用,因此每当Fragment 至少处于生命周期STARTED
状态时。由于在按下后退按钮时片段在测试期间可见,因此始终启用回调时间),将优先于之前在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()
在将片段移动到RESUMED
状态后以单独的onFragment()
运行 - 它接受View
参数,因此不能在onCreateView()
之前使用它
【讨论】:
以上是关于android jetpack 导航仪器测试在返回导航上失败的主要内容,如果未能解决你的问题,请参考以下文章
使用 Jetpack 的 Android 导航组件销毁/重新创建的片段
是否可以使用 Android 导航架构组件(Android Jetpack)有条件地设置 startDestination?