測試導覽功能

請務必在提交前測試應用程式的導覽邏輯,以確認應用程式可正常運作。

Navigation 元件可處理所有作業,包括管理不同目的地之間的導覽、傳遞引數,以及處理 FragmentManager。這些功能已通過嚴格測試,因此無需在應用程式中重新測試。不過,請務必測試片段中應用程式的專用程式碼與其 NavController 之間的互動。本指南將介紹幾種常見導覽情境及其測試方式。

測試片段導覽功能

如要單獨測試片段與其 NavController 的互動情形,不妨採用 Navigation 2.3 以上版本。其中的 TestNavHostController 能提供 API,讓您設定當前目的地,並在 NavController.navigate() 作業之後驗證返回堆疊。

您可以在應用程式模組的 build.gradle 檔案中新增下列依附元件,從而將導覽測試成果新增至專案:

Groovy

dependencies {
  def nav_version = "2.7.7"

  androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
}

Kotlin

dependencies {
  val nav_version = "2.7.7"

  androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
}

假設您要製作一款益智問答遊戲。這個遊戲一開始會進入 title_screen 畫面,當使用者按一下「Play」時,會導覽至 in_game 畫面。

呈現 title_screen 畫面的片段可能如下所示:

Kotlin

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)
        }
    }
}

Java

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 畫面。

結合使用 FragmentScenarioEspressoTestNavHostController,您可以重新建立測試該情境所需的條件,如以下範例所示:

Kotlin

@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)
    }
}

Java

@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 的片段編寫方式如下:

Kotlin

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)
    }
}

Java

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() 之後立即接收回呼,如以下範例所示:

Kotlin

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)
            }
        }
    }
}

Java

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,將控制器連結至自己的測試 LifecycleOwnerViewModelStoreOnBackPressedDispatcher

例如,當測試的片段使用導覽範圍受到限定的 ViewModel 時,您必須在 TestNavHostController 上呼叫 setViewModelStore

Kotlin

val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())

Java

TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());

// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(new ViewModelStore())