Przetestuj nawigację

Ważne jest, aby przed wysłaniem aplikacji przetestować logikę nawigacyjną aplikacji, aby się upewnić, że działa ona zgodnie z oczekiwaniami.

Komponent Nawigacja obsługuje wszystkie zadania związane z zarządzaniem nawigacją między miejscami docelowymi, przekazywaniem argumentów i pracą z elementami FragmentManager. Te funkcje zostały już dokładnie przetestowane, więc nie musisz testować ich ponownie w aplikacji. Ważne są jednak interakcje między kodem konkretnej aplikacji we fragmentach a jego elementem NavController. W tym przewodniku omawiamy kilka typowych scenariuszy nawigacji oraz sposoby ich testowania.

Przetestuj nawigację po fragmentach

Aby w izolacji przetestować interakcje fragmentów z elementami NavController, Nawigacja w wersji 2.3 i nowszych udostępnia interfejs API TestNavHostController, który udostępnia interfejsy API do ustawiania bieżącego miejsca docelowego i weryfikowania stosu wstecznego po operacjach NavController.navigate().

Możesz dodać do projektu artefakt testowania nawigacji, dodając tę zależność do pliku build.gradle modułu aplikacji:

Odlotowy

dependencies {
  def nav_version = "2.7.7"

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

Kotlin

dependencies {
  val nav_version = "2.7.7"

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

Załóżmy, że tworzysz quiz. Gra rozpoczyna się od parametru title_screen, a gdy użytkownik kliknie przycisk zagraj, otworzy się ekran in_game.

Fragment reprezentujący element title_screen może wyglądać np. tak:

Kotlin

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

Java

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

Aby sprawdzić, czy aplikacja prawidłowo przenosi użytkownika do ekranu in_game, gdy użytkownik kliknie Zagraj, test musi sprawdzić, czy ten fragment prawidłowo przenosi element NavController na ekran R.id.in_game.

Używając kombinacji właściwości FragmentScenario, Espresso i TestNavHostController, możesz odtworzyć warunki niezbędne do przetestowania tego scenariusza, jak pokazano w tym przykładzie:

Kotlin

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

Java

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

Powyższy przykład tworzy instancję TestNavHostController i przypisuje ją do fragmentu. Następnie korzysta z Espresso, aby poruszać się po interfejsie, i sprawdza, czy wykonano odpowiednie działanie nawigacyjne.

Tak jak prawdziwy NavController, musisz wywołać setGraph, by zainicjować TestNavHostController. W tym przykładzie testowany fragment był punktem początkowym wykresu. TestNavHostController udostępnia metodę setCurrentDestination, która umożliwia ustawienie bieżącego miejsca docelowego (i opcjonalnie jego argumentów), aby przed rozpoczęciem testu NavController był we właściwym stanie.

W przeciwieństwie do instancji NavHostController, której używałaby NavHostFragment, TestNavHostController nie uruchamia podstawowego działania navigate() (np. FragmentTransaction, które robi FragmentNavigator), gdy wywołujesz navigate() – aktualizuje on tylko stan obiektu TestNavHostController.

Przetestuj interfejs NavigationUI za pomocą FragmentScenariusz

W poprzednim przykładzie wywołanie zwrotne titleScenario.onFragment() jest wywoływane po tym, jak fragment przejdzie przez cykl życia do stanu RESUMED. Do tego czasu widok fragmentu został już utworzony i dołączony, dlatego może być za późno na poprawne testy. Na przykład jeśli używasz parametru NavigationUI z widokami we fragmencie, np. z elementem Toolbar sterowanym przez fragment, możesz wywołać metody konfiguracji za pomocą polecenia NavController, zanim fragment osiągnie stan RESUMED. W związku z tym trzeba znaleźć sposób na ustawienie TestNavHostController na wcześniejszym etapie cyklu życia.

Fragment, który jest właścicielem elementu Toolbar, można zapisać w ten sposób:

Kotlin

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

Java

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

Potrzebny jest tu element NavController utworzony przed wywołaniem funkcji onViewCreated(). Poprzednie podejście onFragment() spowodowałoby TestNavHostController zbyt późne rozpoczęcie cyklu życia, co spowodowałoby błąd wywołania findNavController().

FragmentScenario udostępnia interfejs FragmentFactory, który pozwala rejestrować wywołania zwrotne zdarzeń cyklu życia. Można go połączyć z funkcją Fragment.getViewLifecycleOwnerLiveData(), aby otrzymać wywołanie zwrotne, które następuje bezpośrednio po onCreateView(), jak widać w tym przykładzie:

Kotlin

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

Java

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

Dzięki tej metodzie element NavController jest dostępny przed wywołaniem onViewCreated(), dzięki czemu fragment może używać metod NavigationUI bez awarii.

Testowanie interakcji z wpisami stosu wstecznego

Podczas interakcji z wpisami w stosie wstecznym TestNavHostController umożliwia połączenie kontrolera z własnym testem LifecycleOwner, ViewModelStore i OnBackPressedDispatcher za pomocą interfejsów API, które odziedziczą z NavHostController.

Na przykład podczas testowania fragmentu, który korzysta z ViewModel z zakresem nawigacji, musisz wywołać setViewModelStore w TestNavHostController:

Kotlin

val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

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

Java

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

// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(new ViewModelStore())