من المهم اختبار منطق التنقل في تطبيقك قبل الشحن للتأكّد من عمل تطبيقك على النحو المتوقَّع.
يتولى مكوِّن التنقل جميع أعمال إدارة التنقل بين الوجهات، وتمرير الوسيطات، والعمل باستخدام FragmentManager
.
سبق أن تم اختبار هذه الإمكانات بدقة عالية، لذلك لا داعي لاختبارها مرة أخرى في تطبيقك، ولكن ما يهمّ اختبارها هو التفاعلات بين الرمز البرمجي المحدّد للتطبيق في أجزائه وNavController
الخاصة بها.
يشرح هذا الدليل بعض سيناريوهات التنقل الشائعة وكيفية اختبارها.
اختبار التنقل بين الأجزاء
لاختبار تفاعلات التجزئة مع NavController
بشكل منفصل، يوفّر الانتقال 2.3 والإصدارات الأحدث
TestNavHostController
الذي يوفّر واجهات برمجة تطبيقات لإعداد الوجهة الحالية والتحقّق من تسلسل استدعاء الدوال البرمجية
بعد عمليات
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 وتنتقل إلى شاشة 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
والإسبريسو و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
فقط.
اختبار واجهة مستخدم التنقل باستخدام سيناريو التجزئة
في المثال السابق، يتم استدعاء استدعاء الدالة الذي تم توفيره إلى 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
يمكن استخدامها لتسجيل عمليات معاودة الاتصال لأحداث مراحل النشاط. يمكن دمج هذا الإجراء مع 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; } });
وباستخدام هذه التقنية، تكون السمة NavController
متاحة قبل
استدعاء السمة onViewCreated()
، ما يسمح للجزء باستخدام طرق NavigationUI
بدون تعطُّل.
اختبار التفاعلات مع إدخالات الحزمة الخلفية
عند التفاعل مع إدخالات تسلسل استدعاء الدوال البرمجية في الخلفية، تسمح لك السمة TestNavHostController
بتوصيل وحدة التحكّم باختبارك الخاص بكل من LifecycleOwner
وViewModelStore
وOnBackPressedDispatcher
، وذلك باستخدام واجهات برمجة التطبيقات التي تكتسبها من 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: استخدِم قواعد JUnit 4 مع مكتبات AndroidX Test لتوفير المزيد من المرونة وتقليل الرمز النموذجي المطلوب في الاختبارات.
- اختبار الأجزاء في تطبيقك:
تعرَّف على طريقة اختبار أجزاء التطبيق بمعزل عن بعضها باستخدام
FragmentScenario
. - إعداد مشروع لتطبيق AndroidX Test: تعرَّف على كيفية تحديد المكتبات المطلوبة في ملفات مشاريع التطبيق لاستخدام AndroidX Test.