Перед отправкой важно протестировать логику навигации вашего приложения, чтобы убедиться, что оно работает так, как вы ожидаете.
Компонент навигации выполняет всю работу по управлению навигацией между пунктами назначения, передаче аргументов и работе с FragmentManager
. Эти возможности уже тщательно протестированы, поэтому нет необходимости тестировать их еще раз в вашем приложении. Однако важно протестировать взаимодействие между кодом конкретного приложения в ваших фрагментах и их NavController
. В этом руководстве рассматриваются несколько распространенных сценариев навигации и способы их тестирования.
Навигация по тестовому фрагменту
Чтобы протестировать взаимодействие фрагментов с их NavController
изолированно, Navigation 2.3 и выше предоставляет TestNavHostController
, который предоставляет API для установки текущего пункта назначения и проверки обратного стека после операций NavController.navigate()
.
Вы можете добавить артефакт тестирования навигации в свой проект, добавив следующую зависимость в файл build.gradle
вашего модуля приложения:
dependencies { def nav_version = "2.8.6" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
dependencies { val nav_version = "2.8.6" 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.