請務必在提交前測試應用程式的導覽邏輯,以確認應用程式可正常運作。
Navigation 元件可處理所有作業,包括管理不同目的地之間的導覽、傳遞引數,以及處理 FragmentManager
。這些功能已通過嚴格測試,因此無需在應用程式中重新測試。不過,請務必測試片段中應用程式的專用程式碼與其 NavController
之間的互動。本指南將介紹幾種常見導覽情境及其測試方式。
測試片段導覽功能
如要單獨測試片段與其 NavController
的互動情形,不妨採用 Navigation 2.3 以上版本。其中的 TestNavHostController
能提供 API,讓您設定當前目的地,並在 NavController.navigate()
作業之後驗證返回堆疊。
您可以在應用程式模組的 build.gradle
檔案中新增下列依附元件,從而將導覽測試成果新增至專案:
dependencies { def nav_version = "2.8.6" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
dependencies { val nav_version = "2.8.6" androidTestImplementation("androidx.navigation:navigation-testing:$nav_version") }
假設您要製作一款益智問答遊戲。這個遊戲一開始會進入 title_screen 畫面,當使用者按一下「Play」時,會導覽至 in_game 畫面。
呈現 title_screen 畫面的片段可能如下所示:
class TitleScreen : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = inflater.inflate(R.layout.fragment_title_screen, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<Button>(R.id.play_btn).setOnClickListener {
view.findNavController().navigate(R.id.action_title_screen_to_in_game)
}
}
}
public class TitleScreen extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_title_screen, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
view.findViewById(R.id.play_btn).setOnClickListener(v -> {
Navigation.findNavController(view).navigate(R.id.action_title_screen_to_in_game);
});
}
}
如要測試當使用者點選「Play」後,應用程式是否能夠正確將使用者導覽至 in_game 畫面,您需要在測試中驗證該片段是否能夠將 NavController
正確移動至 R.id.in_game
畫面。
結合使用 FragmentScenario
、Espresso 和 TestNavHostController
,您可以重新建立測試該情境所需的條件,如以下範例所示:
@RunWith(AndroidJUnit4::class)
class TitleScreenTest {
@Test
fun testNavigationToInGameScreen() {
// Create a TestNavHostController
val navController = TestNavHostController(
ApplicationProvider.getApplicationContext())
// Create a graphical FragmentScenario for the TitleScreen
val titleScenario = launchFragmentInContainer<TitleScreen>()
titleScenario.onFragment { fragment ->
// Set the graph on the TestNavHostController
navController.setGraph(R.navigation.trivia)
// Make the NavController available via the findNavController() APIs
Navigation.setViewNavController(fragment.requireView(), navController)
}
// Verify that performing a click changes the NavController’s state
onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click())
assertThat(navController.currentDestination?.id).isEqualTo(R.id.in_game)
}
}
@RunWith(AndroidJUnit4.class)
public class TitleScreenTestJava {
@Test
public void testNavigationToInGameScreen() {
// Create a TestNavHostController
TestNavHostController navController = new TestNavHostController(
ApplicationProvider.getApplicationContext());
// Create a graphical FragmentScenario for the TitleScreen
FragmentScenario<TitleScreen> titleScenario = FragmentScenario.launchInContainer(TitleScreen.class);
titleScenario.onFragment(fragment ->
// Set the graph on the TestNavHostController
navController.setGraph(R.navigation.trivia);
// Make the NavController available via the findNavController() APIs
Navigation.setViewNavController(fragment.requireView(), navController)
);
// Verify that performing a click changes the NavController’s state
onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click());
assertThat(navController.currentDestination.id).isEqualTo(R.id.in_game);
}
}
以上範例建立 TestNavHostController
的例項,並將其指派給片段。接著使用 Espresso 驅動 UI,並驗證是否執行了適當的導覽動作。
和真正的 NavController
一樣,必須呼叫 setGraph
才能初始化 TestNavHostController
。在本範例中,要測試的片段是圖表的起始目的地。TestNavHostController
提供 setCurrentDestination
方法,讓您能夠設定當前目的地 (同時還可選擇設定該目的地的引數),以便在測試開始之前 NavController
處於正確狀態。
與 NavHostFragment
將使用的 NavHostController
例項不同,當您呼叫 navigate()
時,TestNavHostController
不會觸發下層的 navigate()
行為 (如 FragmentNavigator
執行的 FragmentTransaction
),它只會更新 TestNavHostController
的狀態。
使用 FragmentScenario 測試 NavigationUI
在上一範例中,在片段移至其生命週期的 RESUMED
狀態後,會呼叫提供給 titleScenario.onFragment()
的回呼。此時已建立並附加片段的檢視區塊,它處於生命週期中較晚的階段,可能無法正確進行測試。舉例來說,搭配片段中的檢視區塊使用 NavigationUI
時 (例如由片段控制的 Toolbar
),您可在片段進入 RESUMED
狀態前,使用 NavController
呼叫設定方法。因此,您需要採取方法在生命週期的早期階段設定 TestNavHostController
。
本身擁有 Toolbar
的片段編寫方式如下:
class TitleScreen : Fragment(R.layout.fragment_title_screen) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = view.findNavController()
view.findViewById<Toolbar>(R.id.toolbar).setupWithNavController(navController)
}
}
public class TitleScreen extends Fragment {
public TitleScreen() {
super(R.layout.fragment_title_screen);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
NavController navController = Navigation.findNavController(view);
view.findViewById(R.id.toolbar).setupWithNavController(navController);
}
}
呼叫 onViewCreated()
的時候,我們需要已建立完成的 NavController
。使用之前的 onFragment()
方法會在生命週期的較晚階段設定 TestNavHostController
,導致 findNavController()
呼叫失敗。
FragmentScenario
提供 FragmentFactory
介面,可用於註冊生命週期事件的回呼。可將其與 Fragment.getViewLifecycleOwnerLiveData()
結合使用,在 onCreateView()
之後立即接收回呼,如以下範例所示:
val scenario = launchFragmentInContainer {
TitleScreen().also { fragment ->
// In addition to returning a new instance of our Fragment,
// get a callback whenever the fragment’s view is created
// or destroyed so that we can set the NavController
fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
if (viewLifecycleOwner != null) {
// The fragment’s view has just been created
navController.setGraph(R.navigation.trivia)
Navigation.setViewNavController(fragment.requireView(), navController)
}
}
}
}
FragmentScenario<TitleScreen> scenario =
FragmentScenario.launchInContainer(
TitleScreen.class, null, new FragmentFactory() {
@NonNull
@Override
public Fragment instantiate(@NonNull ClassLoader classLoader,
@NonNull String className,
@Nullable Bundle args) {
TitleScreen titleScreen = new TitleScreen();
// In addition to returning a new instance of our fragment,
// get a callback whenever the fragment’s view is created
// or destroyed so that we can set the NavController
titleScreen.getViewLifecycleOwnerLiveData().observeForever(new Observer<LifecycleOwner>() {
@Override
public void onChanged(LifecycleOwner viewLifecycleOwner) {
// The fragment’s view has just been created
if (viewLifecycleOwner != null) {
navController.setGraph(R.navigation.trivia);
Navigation.setViewNavController(titleScreen.requireView(), navController);
}
}
});
return titleScreen;
}
});
使用這項技術時,可在呼叫 onViewCreated()
之前使用 NavController
,讓片段能夠使用 NavigationUI
方法而不會異常終止。
對返回堆疊項目的互動情形進行測試
當與返回堆疊項目互動時,TestNavHostController
讓您能夠使用從 NavHostController
處沿用而來的 API,將控制器連結至自己的測試 LifecycleOwner
、ViewModelStore
和 OnBackPressedDispatcher
。
例如,當測試的片段使用導覽範圍受到限定的 ViewModel 時,您必須在 TestNavHostController
上呼叫 setViewModelStore
:
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())
TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());
// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(new ViewModelStore())
相關主題
- 建構已檢測的單元測試:瞭解如何在 Android 裝置上設定已檢測的測試套件並執行測試。
- Espresso:使用 Espresso 測試應用程式的 UI。
- JUnit4 規則搭配 AndroidX Test:搭配使用 JUnit 4 規則與 AndroidX Test 程式庫,從而提供更多彈性,並減少測試中所需的樣板程式碼。
- 測試應用程式的片段:瞭解如何使用
FragmentScenario
單獨測試應用程式片段。 - 設定用於 AndroidX Test 的專案:瞭解如何在應用程式的專案檔案中宣告使用 AndroidX Test 所需的程式庫。