تست ناوبری

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

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

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

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

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

شیار

dependencies {
  def nav_version = "2.8.4"

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

کاتلین

dependencies {
  val nav_version = "2.8.4"

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

فرض کنید در حال ساخت یک بازی چیزهای بی اهمیت هستید. بازی با یک title_screen شروع می شود و وقتی کاربر روی play کلیک می کند به صفحه in_game هدایت می شود.

قطعه نشان دهنده 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 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)
    }
}

جاوا

@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 برای هدایت UI استفاده می کند و تأیید می کند که عملکرد ناوبری مناسب انجام شده است.

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

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

NavigationUI را با FragmentScenario تست کنید

در مثال قبلی، callback ارائه شده به 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() در دسترس است و به قطعه اجازه می‌دهد از روش‌های 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())