חשוב לבדוק את לוגיקת הניווט באפליקציה לפני ששולחים אותה, כדי לוודא שהיא פועלת כמו שציפיתם.
רכיב הניווט מטפל בכל הפעולות שקשורות לניהול הניווט בין יעדים, להעברת ארגומנטים ולעבודה עם FragmentManager. היכולות האלה כבר נבדקו באופן יסודי, ולכן אין צורך לבדוק אותן שוב באפליקציה. עם זאת, חשוב לבדוק את האינטראקציות בין הקוד הספציפי לאפליקציה בקטעי הקוד לבין NavController.
בדיקה בבידוד
כדי לבדוק אינטראקציות של קטעי קוד עם NavController בבידוד, גרסה 2.3 ואילך של Navigation מספקת TestNavHostController שמספקת ממשקי API להגדרת היעד הנוכחי ולאימות היסטוריית החזרה אחורה אחרי פעולות NavController.navigate().
כדי להוסיף את ארטיפקט הבדיקה של Navigation לפרויקט, מוסיפים את התלות הבאה לקובץ build.gradle של מודול האפליקציה:
Groovy
dependencies { def nav_version = "2.9.8" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.8" androidTestImplementation("androidx.navigation:navigation-testing:$nav_version") }
אפשר לשחק במשחק טריוויה. המשחק מתחיל עם מסך הפתיחה ועובר למסך בתוך המשחק כשהמשתמש לוחץ על 'הפעלה'.
הקטע שמייצג את 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 כשלוחצים על Play, הבדיקה צריכה לוודא שהקטע הזה מעביר את 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 using 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
fun 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 using 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 מספקת method setCurrentDestination שמאפשרת להגדיר את היעד הנוכחי (ואופציונלית, ארגומנטים ליעד הזה) כדי שה-NavController יהיה במצב הנכון לפני שהבדיקה מתחילה.
שלא כמו מופע NavHostController ש-NavHostFragment ישתמש בו, TestNavHostController לא מפעיל את ההתנהגות הבסיסית של navigate() (כמו FragmentTransaction ש-FragmentNavigator מפעיל) כשמפעילים את navigate() – הוא רק מעדכן את המצב של TestNavHostController.
בדיקת NavigationUI באמצעות FragmentScenario
בדוגמה הקודמת, הקריאה החוזרת שסופקה ל-titleScenario.onFragment() מתבצעת אחרי שהקטע עבר את מחזור החיים שלו למצב RESUMED. בשלב הזה, התצוגה של ה-Fragment כבר נוצרה וצורפה, ולכן יכול להיות שזה מאוחר מדי במחזור החיים כדי לבצע בדיקה תקינה. לדוגמה, כשמשתמשים ב-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() עם onCreateView() כדי לקבל קריאה חוזרת (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(), וכך אפשר להשתמש בשיטות NavigationUI בלי שהאפליקציה תקרוס.
בדיקת אינטראקציות עם רשומות במקבץ פעילויות קודמות (back stack)
כשמבצעים אינטראקציה עם הרשומות במקבץ הפעילויות הקודמות (back stack), אפשר להשתמש ב-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())
נושאים קשורים
- יצירת בדיקות יחידה עם מכשור – כאן מוסבר איך להגדיר את חבילת מקרים לבדיקה עם מכשור ולהריץ בדיקות במכשיר עם Android.
- Espresso – בדיקת ממשק המשתמש של האפליקציה באמצעות Espresso.
- כללי JUnit4 עם AndroidX Test – שימוש בכללי JUnit 4 עם ספריות AndroidX Test מספק גמישות רבה יותר ומפחית את כמות הקוד שחוזר על עצמו שנדרשת בבדיקות.
- בדיקת פרגמנטים באפליקציה – כאן מוסבר איך לבדוק פרגמנטים באפליקציה בבידוד באמצעות
FragmentScenario. - הגדרת פרויקט ל-AndroidX Test – במאמר הזה מוסבר איך להצהיר על הספריות הנדרשות בקובצי הפרויקט של האפליקציה כדי להשתמש ב-AndroidX Test.