# Unit Test - 使用hilt測試7 - Testing Navigation --- ## 開始吧 ### 情境 * 要透過UI操作,驗證測試頁面是否正確導航至指定的頁面 * Espresso 主要模擬UI的操作 * Mockito 產生假物件與驗證 ### res底下新增nev_graph * 新增Destination並加入action ![](https://i.imgur.com/1Bmy1Xv.png) * 設定navHost ```kotlin= // activity_main.xml <fragment android:id="@+id/navHostFragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_graph"/> ``` ### 設定對應的navigate action * 設定shoppingFragment導航至addShoppingItemFragment的觸發按鈕 ```kotlin= class ShoppingFragment : Fragment(R.layout.fragment_shopping) { lateinit var viewModel: ShoppingViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel = ViewModelProvider(requireActivity()).get(ShoppingViewModel::class.java) //對floating button設置點擊事件->觸發導航至下一個頁面 fabAddShoppingItem.setOnClickListener { findNavController().navigate( ShoppingFragmentDirections.actionShoppingFragmentToAddShoppingItemFragment()) } } } ``` ![](https://i.imgur.com/j4HKjak.png) * 設定addShoppingItemFragment導航至imagePickFragment的觸發listener ```kotlin= class AddShoppingItemFragment: Fragment(R.layout.fragment_add_shopping_item) { lateinit var viewModel: ShoppingViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel = ViewModelProvider(requireActivity()).get(ShoppingViewModel::class.java) //設定對應的導航listener ivShoppingImage.setOnClickListener { findNavController().navigate( AddShoppingItemFragmentDirections.actionAddShoppingItemFragmentToImagePickFragment() ) } //註冊callback在按返回鍵,自動清空ImageUrl val callback = object : OnBackPressedCallback(true){ override fun handleOnBackPressed() { viewModel.setCurImageUrl("") findNavController().popBackStack() } } requireActivity().onBackPressedDispatcher.addCallback(callback) } } ``` ### 寫測試吧 1. **測試shoppingFragment到addShoppingFragment頁面** * 使用mockito做一個mock的navController * 再使用上一篇的launchFragmentInHiltContainer建立含有@Android Entry Point的fragment * 再將navController和fragment做連結,就可以透過controller控制這個fragment與其childView * 使用Espresso指定UI的元件與其action,這邊我們指向到floating button 執行click * 使用verify驗證我們mock的navController中,navigate方法是不是導航至我們所指定的 ```kotlin= @MediumTest @HiltAndroidTest @ExperimentalCoroutinesApi class ShoppingFragmentTest { @get:Rule var hiltRule = HiltAndroidRule(this) @Before fun setup() { hiltRule.inject() } @Test fun clickAddShoppingItemButton_navigateToAddShoppingItemFragment() { val navController = mock(NavController::class.java) launchFragmentInHiltContainer<ShoppingFragment> { Navigation.setViewNavController(requireView(), navController) } onView(withId(R.id.fabAddShoppingItem)).perform(click()) verify(navController).navigate( ShoppingFragmentDirections.actionShoppingFragmentToAddShoppingItemFragment() ) } } ``` 2. **測試addShoppingFragment到imagePickFragment頁面** * 驗證popBackStack是否有正確執行 * 驗證點選ivShoppingImage是否正確導航到指定頁面 ```kotlin= @ExperimentalCoroutinesApi @MediumTest @HiltAndroidTest class AddShoppingItemFragmentTest { @get:Rule var hiltRule = HiltAndroidRule(this) @Before fun setup(){ hiltRule.inject() } @Test fun pressBackButton_popBackStack(){ val navController = Mockito.mock(NavController::class.java) launchFragmentInHiltContainer<AddShoppingItemFragment> { Navigation.setViewNavController(requireView(), navController) } pressBack() verify(navController).popBackStack() } @Test fun clickivShoppingImage_navigeteToImagePickFragment(){ val navController = Mockito.mock(NavController::class.java) launchFragmentInHiltContainer<AddShoppingItemFragment> { Navigation.setViewNavController(requireView(), navController) } //指定ivShoppingImage執行click動作 onView(withId(R.id.ivShoppingImage)).perform(click()) //驗證是否導航至指定頁面 verify(navController).navigate( AddShoppingItemFragmentDirections.actionAddShoppingItemFragmentToImagePickFragment() ) } } ``` 3. **額外驗證popBackStack之後curImageUrl是否設定為empty** * 在測試viewModel有介紹使用假的repo實例viewModel * 所以要把之前在test寫的fakeRepository複製到AndroidTest這邊的路徑 * 因為curImageUrl是在背景執行緒更改,所以要加入Rule * mock AddShoppingItemFragment並將我們使用fakeRepository產生的實例指派給mock的fragment內的viewModel * 執行pressBack()之後驗證curImageUrl這個liveData的值是否為空 ```kotlin= @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Test fun imageUrlIsEmpty_afterPooBackStack(){ val testviewModel = ShoppingViewModel(FakeRepository()) val navController = mock(NavController::class.java) launchFragmentInHiltContainer<AddShoppingItemFragment> { Navigation.setViewNavController(requireView(), navController) viewModel = testviewModel } pressBack() val value = testviewModel.curImageUrl.getOrAwaitValue() assertThat(value).isEmpty() } ``` 參考資料 [ Philipp Lackner's channel](https://www.youtube.com/watch?v=uUmFfZDoOTY) ###### tags: `test` `Unit Test` `kotlin`