Перед отправкой важно протестировать логику навигации вашего приложения, чтобы убедиться, что оно работает так, как вы ожидаете.
Компонент навигации выполняет всю работу по управлению навигацией между пунктами назначения, передаче аргументов и работе с FragmentManager
. Эти возможности уже тщательно протестированы, поэтому нет необходимости тестировать их еще раз в вашем приложении. Однако важно протестировать взаимодействие между кодом конкретного приложения в ваших фрагментах и их NavController
. В этом руководстве рассматриваются несколько распространенных сценариев навигации и способы их тестирования.
Навигация по тестовому фрагменту
Чтобы протестировать взаимодействие фрагментов с их NavController
изолированно, Navigation 2.3 и выше предоставляет TestNavHostController
, который предоставляет API для установки текущего пункта назначения и проверки обратного стека после операций NavController.navigate()
.
Вы можете добавить артефакт тестирования навигации в свой проект, добавив следующую зависимость в файл build.gradle
вашего модуля приложения:
Groovy
dependencies { def nav_version = "2.8.4" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.4" androidTestImplementation("androidx.navigation:navigation-testing:$nav_version") }
Допустим, вы создаете викторину. Игра начинается с title_screen и переходит к экрану 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); }); } }
Чтобы проверить, что приложение правильно перемещает пользователя на экран in_game , когда пользователь нажимает Play , ваш тест должен убедиться, что этот фрагмент правильно перемещает 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 для управления пользовательским интерфейсом и проверяет, выполнено ли соответствующее действие навигации.
Как и в случае с настоящим NavController
, вы должны вызвать setGraph
для инициализации TestNavHostController
. В этом примере тестируемый фрагмент был начальной точкой нашего графа. TestNavHostController
предоставляет метод setCurrentDestination
, который позволяет вам установить текущий пункт назначения (и, при необходимости, аргументы для этого пункта назначения), чтобы NavController
находился в правильном состоянии перед началом теста.
В отличие от экземпляра NavHostController
, который будет использовать NavHostFragment
, TestNavHostController
не запускает базовое поведение navigate()
(например, FragmentTransaction
, которое выполняет FragmentNavigator
) при вызове navigate()
— он только обновляет состояние TestNavHostController
.
Тестирование пользовательского интерфейса навигации с помощью FragmentScenario
В предыдущем примере обратный вызов, предоставленный titleScenario.onFragment()
, вызывается после того, как фрагмент прошел свой жизненный цикл в состояние RESUMED
. К этому времени представление фрагмента уже создано и прикреплено, поэтому может быть слишком поздно в жизненном цикле для правильного тестирования. Например, при использовании NavigationUI
с представлениями в вашем фрагменте, например с Toolbar
, управляемой вашим фрагментом, вы можете вызвать методы настройки с помощью NavController
до того, как фрагмент достигнет состояния RESUMED
. Таким образом, вам нужен способ установить 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
позволяет подключить контроллер к вашему собственному тесту LifecycleOwner
, ViewModelStore
и OnBackPressedDispatcher
, используя API-интерфейсы, которые он наследует от NavHostController
.
Например, при тестировании фрагмента, который использует 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())
Связанные темы
- Создавайте инструментированные модульные тесты . Узнайте, как настроить набор инструментальных тестов и запускать тесты на устройстве Android.
- Эспрессо . Проверьте пользовательский интерфейс вашего приложения с помощью Espresso.
- Правила JUnit4 с тестом AndroidX . Используйте правила JUnit 4 с библиотеками тестов AndroidX, чтобы обеспечить большую гибкость и сократить количество шаблонного кода, необходимого в тестах.
- Тестируйте фрагменты вашего приложения . Узнайте, как тестировать фрагменты вашего приложения изолированно с помощью
FragmentScenario
. - Настройка проекта для AndroidX Test . Узнайте, как объявить необходимые библиотеки в файлах проекта вашего приложения для использования AndroidX Test.