اختبار التنقل بين أجزاء

من المهم اختبار منطق التنقّل في تطبيقك قبل إطلاقه للتأكّد من أنّه يعمل على النحو المتوقّع.

يتولّى مكوّن Navigation جميع مهام إدارة التنقّل بين الوجهات وتمرير الوسيطات والتعامل مع FragmentManager. تم اختبار هذه الإمكانات بدقة من قبل، لذا ما مِن حاجة إلى اختبارها مرة أخرى في تطبيقك. ومع ذلك، من المهم اختبار التفاعلات بين الرمز البرمجي الخاص بالتطبيق في أجزائك وNavController.

الاختبار بشكل منفصل

لاختبار تفاعلات الأجزاء مع NavController بشكل منفصل، يوفّر الإصدار 2.3 من Navigation والإصدارات الأحدث TestNavHostController الذي يوفّر واجهات برمجة تطبيقات لـ ضبط الوجهة الحالية والتحقّق من سجلّ الرجوع بعد NavController.navigate() عمليات.

يمكنك إضافة عنصر Navigation Testing إلى مشروعك من خلال إضافة التبعية التالية في ملف 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 وتنتقل إلى شاشة in_game عندما ينقر المستخدم على "تشغيل".

مسار التنقّل في لعبة المعلومات العامة
الشكل 1. مسار التنقّل في لعبة المعلومات العامة

قد يبدو الجزء الذي يمثّل 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 يوفّر طريقة setCurrentDestination تتيح لك ضبط الوجهة الحالية (ويمكنك اختياريًا ضبط وسيطات لهذه الوجهة) حتى يكون NavController في الحالة الصحيحة قبل بدء الاختبار.

على عكس مثيل NavHostController الذي يستخدمه NavHostFragment، TestNavHostController لا يؤدي إلى تفعيل سلوك navigate() الأساسي (مثل FragmentTransaction الذي يفعّله FragmentNavigator) عند استدعاء navigate()، بل يؤدي فقط إلى تعديل حالة TestNavHostController.

اختبار NavigationUI باستخدام FragmentScenario

في المثال السابق، يتم استدعاء رد الاتصال المقدَّم إلى 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())