تست ناوبری قطعه

مهم است که قبل از انتشار، منطق ناوبری برنامه خود را آزمایش کنید تا تأیید کنید که برنامه شما مطابق انتظار شما کار می‌کند.

کامپوننت Navigation تمام کارهای مدیریت ناوبری بین مقاصد، ارسال آرگومان‌ها و کار با FragmentManager را انجام می‌دهد. این قابلیت‌ها قبلاً به طور دقیق آزمایش شده‌اند، بنابراین نیازی به آزمایش مجدد آنها در برنامه شما نیست. با این حال، آنچه برای آزمایش مهم است، تعاملات بین کد مخصوص برنامه در قطعات شما و NavController آنها است.

تست در انزوا

برای آزمایش تعاملات قطعه کد با NavController آنها به صورت جداگانه، Navigation 2.3 و بالاتر یک TestNavHostController ارائه می‌دهد که APIهایی را برای تنظیم مقصد فعلی و تأیید پشته پشتی پس از عملیات NavController.navigate() ارائه می‌دهد.

شما می‌توانید با اضافه کردن وابستگی زیر در فایل build.gradle ماژول app خود، ابزار Navigation Testing را به پروژه خود اضافه کنید:

گرووی

dependencies {
  def nav_version = "2.9.8"

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

کاتلین

dependencies {
  val nav_version = "2.9.8"

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

یک بازی چیزهای بی اهمیت را در نظر بگیرید. بازی با یک صفحه عنوان شروع می‌شود و وقتی کاربر روی پخش کلیک می‌کند، به صفحه درون بازی می‌رود.

جریان ناوبری بازی Trivia
شکل ۱. جریان ناوبری بازی Trivia.

قطعه‌ای که title_screen را نشان می‌دهد ممکن است چیزی شبیه به این باشد:

کاتلین

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

جاوا

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);
        });
    }
}

برای آزمایش اینکه برنامه هنگام کلیک کاربر روی Play ، کاربر را به درستی به صفحه in_game هدایت می‌کند، تست شما باید تأیید کند که این قطعه به درستی NavController را به صفحه R.id.in_game منتقل می‌کند.

با استفاده از ترکیبی از FragmentScenario ، Espresso و TestNavHostController ، می‌توانید شرایط لازم برای آزمایش این سناریو را همانطور که در مثال زیر نشان داده شده است، بازسازی کنید:

کاتلین

@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)
    }
}

جاوا

@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 ایجاد می‌کند و آن را به fragment اختصاص می‌دهد. سپس از Espresso برای هدایت رابط کاربری استفاده می‌کند و تأیید می‌کند که عمل ناوبری مناسب انجام شده است.

درست مانند یک NavController واقعی، شما باید setGraph برای مقداردهی اولیه TestNavHostController فراخوانی کنید. در این مثال، قطعه‌ای که آزمایش می‌شود، مقصد شروع گراف ما بود. TestNavHostController یک متد setCurrentDestination ارائه می‌دهد که به شما امکان می‌دهد مقصد فعلی (و به صورت اختیاری، آرگومان‌هایی برای آن مقصد) را تنظیم کنید تا NavController قبل از شروع آزمایش شما در وضعیت صحیح باشد.

برخلاف یک نمونه NavHostController که یک NavHostFragment از آن استفاده می‌کند، TestNavHostController هنگام فراخوانی navigate() رفتار زیربنایی (مانند FragmentTransaction که FragmentNavigator انجام می‌دهد navigate() را فعال نمی‌کند - فقط وضعیت TestNavHostController را به‌روزرسانی می‌کند.

تست رابط کاربری ناوبری (NavigationUI) با FragmentScenario

در مثال قبلی، تابع فراخوانی ارائه شده به titleScenario.onFragment() پس از اینکه فرگمنت چرخه حیات خود را به حالت RESUMED طی کرد، فراخوانی می‌شود. در این زمان، نمای فرگمنت قبلاً ایجاد و پیوست شده است، بنابراین ممکن است در چرخه حیات برای آزمایش صحیح خیلی دیر شده باشد. به عنوان مثال، هنگام استفاده از NavigationUI با نماها در فرگمنت خود، مانند یک Toolbar که توسط فرگمنت شما کنترل می‌شود، می‌توانید قبل از اینکه فرگمنت به حالت RESUMED برسد، متدهای راه‌اندازی را با NavController خود فراخوانی کنید. بنابراین، به روشی نیاز دارید تا TestNavHostController خود را در چرخه حیات زودتر تنظیم کنید.

یک فرگمنت که Toolbar مخصوص به خود را دارد، می‌تواند به صورت زیر نوشته شود:

کاتلین

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

جاوا

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() می‌آید، دریافت کند، همانطور که در مثال زیر نشان داده شده است:

کاتلین

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

جاوا

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() در دسترس است و به fragment اجازه می‌دهد بدون از کار افتادن، از متدهای NavigationUI استفاده کند.

آزمایش تعاملات با ورودی‌های پشته پشتی

هنگام تعامل با ورودی‌های پشته پشتی ، TestNavHostController به شما امکان می‌دهد تا با استفاده از APIهایی که از NavHostController به ارث می‌برد، کنترلر را به LifecycleOwner ، ViewModelStore و OnBackPressedDispatcher تست خود متصل کنید.

برای مثال، هنگام تست یک فرگمنت که از ViewModel با دامنه ناوبری استفاده می‌کند، باید setViewModelStore در TestNavHostController فراخوانی کنید:

کاتلین

val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())

جاوا

TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());

// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(new ViewModelStore())
  • ساخت تست‌های واحد ابزار دقیق - یاد بگیرید که چگونه مجموعه تست ابزار دقیق خود را تنظیم کنید و تست‌ها را روی یک دستگاه مبتنی بر اندروید اجرا کنید.
  • Espresso - رابط کاربری برنامه خود را با Espresso تست کنید.
  • قوانین JUnit4 به همراه AndroidX Test - از قوانین JUnit 4 به همراه کتابخانه‌های AndroidX Test استفاده کنید تا انعطاف‌پذیری بیشتری داشته باشید و کدهای تکراری مورد نیاز در تست‌ها را کاهش دهید.
  • تست قطعات برنامه - یاد بگیرید که چگونه قطعات برنامه خود را به صورت جداگانه با FragmentScenario تست کنید.
  • راه‌اندازی پروژه برای AndroidX Test - یاد بگیرید که چگونه کتابخانه‌های مورد نیاز را در فایل‌های پروژه برنامه خود برای استفاده از AndroidX Test اعلام کنید.