تجربة التنقّل

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

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

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

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

يمكنك إضافة أداة اختبار التنقل إلى مشروعك عن طريق إضافة الاعتمادية التالية في ملف build.gradle الخاص بوحدة تطبيقك:

Groovy

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

اختبار واجهة المستخدم للتنقل مع سيناريو جزء

في المثال السابق، تم تقديم طلب معاودة الاتصال إلى 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

فعلى سبيل المثال، عند اختبار جزء يستخدم العرض الخاص بنموذج التنقّل في نطاق التنقّل، يجب الاتصال 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())