Тестовая навигация

Перед отправкой важно протестировать логику навигации вашего приложения, чтобы убедиться, что оно работает так, как вы ожидаете.

Компонент навигации выполняет всю работу по управлению навигацией между пунктами назначения, передаче аргументов и работе с 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 и переходит к экрану 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())