בדיקת הניווט

חשוב לבדוק את לוגיקת הניווט של האפליקציה לפני המשלוח כדי לוודא שהאפליקציה פועלת כצפוי.

רכיב הניווט מטפל בכל העבודה של ניהול הניווט בין יעדים, העברת ארגומנטים ועבודה עם FragmentManager היכולות האלה כבר נבדקו בקפדנות, לכן לא צריך לבצע בדיקות אותם שוב באפליקציה. עם זאת, מה שחשוב לבדוק הן את האינטראקציות בין הקוד הספציפי לאפליקציה במקטעים שלך NavController במדריך הזה מפורטים תרחישי ניווט נפוצים ונסביר איך לבדוק אותם.

בדיקת הניווט במקטעים

כדי לבדוק אינטראקציות עם מקטעים עם NavController בנפרד, ניווט בגרסה 2.3 ואילך מספק TestNavHostController שמספק ממשקי API להגדרת היעד הנוכחי ולאימות סטאק אחרי NavController.navigate() ב-AI.

כדי להוסיף לפרויקט ארטיפקט של בדיקת הניווט, צריך להוסיף את התלות הבאה בקובץ build.gradle של מודול האפליקציה:

מגניב

dependencies {
  def nav_version = "2.8.0"

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

Kotlin

dependencies {
  val nav_version = "2.8.0"

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

נניח שאתם מפתחים משחק טריוויה. המשחק מתחיל title_screen ועוברים למסך 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);
        });
    }
}

כדי לבדוק שהאפליקציה מנווטת באופן תקין את המשתמש למסך in_game המשתמש לוחץ על הפעלה, הבדיקה צריכה לאמת שהמקטע הזה מעביר את NavController בצורה נכונה למסך R.id.in_game.

באמצעות שילוב של FragmentScenario, Espresso, ו-TestNavHostController, אפשר ליצור מחדש את התנאים הדרושים בתרחיש הזה, כפי שאפשר לראות בדוגמה הבאה:

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 כדי להפעיל את ממשק המשתמש ומאמתת ננקטת פעולת ניווט מתאימה.

בדיוק כמו NavController אמיתי, צריך לקרוא ל-setGraph כדי לאתחל TestNavHostController. בדוגמה זו, המקטע שנבדק היעד ההתחלתי של התרשים. TestNavHostController מספק setCurrentDestination שמאפשרת להגדיר את היעד הנוכחי (ואופציונלית, ארגומנטים עבור יעד זה) כך שה-NavController נמצא לפני שהבדיקה מתחילה.

בניגוד למופע של NavHostController שבו משתמש NavHostFragment, TestNavHostController לא מפעיל את ה-navigate() הבסיסי התנהגות (כמו FragmentTransaction ש-FragmentNavigator עושה) כשקוראים לפונקציה navigate(), המערכת מעדכנת רק את המצב של TestNavHostController.

בדיקת ממשק הניווט של המשתמש עם FragmentScenario

בדוגמה הקודמת, הקריאה החוזרת (callback) סופקה ל-titleScenario.onFragment() נקראת אחרי שהמקטע עבר במחזור החיים שלו אל RESUMED . בשלב הזה, תצוגת המקטע כבר נוצרה וצורפה. ולכן ייתכן שיהיה מאוחר מדי במחזור החיים כדי לבדוק כראוי. לדוגמה, כשמשתמשים NavigationUI עם צפיות במקטע שלך, למשל עם Toolbar מבוקרת לפי המקטע שלך, ניתן לקרוא לשיטות הגדרה באמצעות NavController לפני המקטע מגיע למצב RESUMED. לכן צריך דרך להגדיר 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);
    }
}

כאן אנחנו צריכים את ה-NavController שנוצר עד למועד הקריאה של onViewCreated(). אם תשתמש בגישה הקודמת של onFragment(), המערכת תגדיר את TestNavHostController בשלב מאוחר מדי במחזור החיים, מה שגורם לקריאה של findNavController() להיכשל.

FragmentScenario מציע FragmentFactory ממשק שבו אפשר להשתמש כדי לרשום קריאות חוזרות (callback) לאירועים במחזור חיים. מי יכול ישולבו עם Fragment.getViewLifecycleOwnerLiveData() כדי לקבל קריאה חוזרת (callback) שמיד לאחר 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;
    }
});

באמצעות השיטה הזו, NavController זמין לפני הפונקציה onViewCreated() נקראת, כך שהמקטע יכול להשתמש ב-methods NavigationUI בלי לקרוס.

בדיקת אינטראקציות עם רשומות של מקבץ אחורי

באינטראקציה עם הרשומות של המקבץ 'הקודם': באמצעות TestNavHostController אפשר לחבר את הבקר בדיקה של LifecycleOwner, ViewModelStore ו-OnBackPressedDispatcher באמצעות באמצעות ממשקי ה-API שהיא מקבלת בירושה NavHostController.

לדוגמה, בעת בדיקת מקטע שמשתמש ב- היקף התצוגה של ViewModel, צריך להתקשר setViewModelStore ב-TestNavHostController:

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