Przetestuj nawigację po fragmentach

Zanim opublikujesz aplikację, przetestuj logikę nawigacji, aby sprawdzić, czy działa ona zgodnie z oczekiwaniami.

Komponent Navigation zajmuje się zarządzaniem nawigacją między miejscami docelowymi, przekazywaniem argumentów i współpracą z FragmentManager. Te funkcje są już dokładnie przetestowane, więc nie musisz ich ponownie testować w aplikacji. Ważne jest jednak, aby przetestować interakcje między kodem specyficznym dla aplikacji w fragmentach a ich NavController.

Testowanie w izolacji

Aby przetestować interakcje fragmentów w izolacji, biblioteka NavController w wersji 2.3 i nowszych udostępnia klasę TestNavHostController, która zawiera interfejsy API do ustawiania bieżącego miejsca docelowego i weryfikowania listy wstecznej po operacjach NavController.navigate().

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

Groovy

dependencies {
  def nav_version = "2.9.8"

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

Kotlin

dependencies {
  val nav_version = "2.9.8"

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

Zagraj w quiz. Gra zaczyna się od ekranu tytułowego, a gdy użytkownik kliknie przycisk odtwarzania, przechodzi do ekranu w grze.

Ścieżka nawigacji w quizie
Rysunek 1. Przepływ nawigacji w quizie.

Fragment reprezentujący title_screen może wyglądać 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 na ekran in_game, gdy kliknie on Graj, test musi zweryfikować, czy ten fragment prawidłowo przenosi NavController na ekran R.id.in_game.

Używając kombinacji FragmentScenario, EspressoTestNavHostController, 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 using 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
    fun 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 using 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);
    }
}

W poprzednim przykładzie tworzona jest instancja klasy TestNavHostController i przypisywana do fragmentu. Następnie używa Espresso do sterowania interfejsem i sprawdza, czy wykonano odpowiednie działanie nawigacyjne.

Podobnie jak w przypadku prawdziwego NavController, musisz wywołać setGraph, aby zainicjować TestNavHostController. W tym przykładzie testowany fragment to miejsce docelowe na początku naszego wykresu. TestNavHostController udostępnia metodę setCurrentDestination, która pozwala ustawić bieżące miejsce docelowe (i opcjonalnie argumenty dla tego miejsca docelowego), aby przed rozpoczęciem testu NavController znajdował się w odpowiednim stanie.

W przeciwieństwie do NavHostController, z którego korzysta NavHostFragment, wywołanie TestNavHostController nie powoduje uruchomienia bazowego zachowania navigate() (np. FragmentTransaction, które jest uruchamiane przez FragmentNavigator). Wywołanie navigate() powoduje tylko aktualizację stanu TestNavHostController.

Testowanie NavigationUI za pomocą FragmentScenario

W powyższym przykładzie wywołanie zwrotne przekazane do funkcji titleScenario.onFragment() jest wywoływane po przejściu fragmentu przez cykl życia do stanu RESUMED. W tym czasie widok fragmentu jest już utworzony i dołączony, więc może być za późno na prawidłowe przetestowanie go w cyklu życia. Na przykład podczas korzystania z NavigationUI z widokami w fragmencie, np. z Toolbar kontrolowanym przez fragment, możesz wywoływać metody konfiguracji za pomocą NavController, zanim fragment osiągnie stan RESUMED. Dlatego musisz mieć możliwość ustawienia wartości parametru TestNavHostController na wcześniejszym etapie cyklu życia.

Fragment, który ma własny 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);
    }
}

W tym miejscu potrzebujemy NavController utworzonego do czasu wywołania onViewCreated(). Użycie poprzedniego podejścia onFragment() spowodowałoby ustawienie TestNavHostController zbyt późno w cyklu życia, co spowodowałoby niepowodzenie wywołania findNavController().

FragmentScenario udostępnia interfejs FragmentFactory, który umożliwia rejestrowanie wywołań zwrotnych dla zdarzeń cyklu życia. Możesz połączyć tę funkcję z parametrem Fragment.getViewLifecycleOwnerLiveData(), aby otrzymać wywołanie zwrotne natychmiast po onCreateView(), jak pokazano 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 technice obiekt NavController jest dostępny przed wywołaniem funkcji onViewCreated(), co pozwala fragmentowi używać metod NavigationUI bez powodowania awarii.

Testowanie interakcji z wpisami na stosie wstecznym

Podczas interakcji z elementami listy wstecznej element TestNavHostController umożliwia połączenie kontrolera z własnymi testami LifecycleOwner, ViewModelStoreOnBackPressedDispatcher za pomocą interfejsów API odziedziczonych z NavHostController.

Jeśli na przykład testujesz fragment, który korzysta z obiektu ViewModel o zakresie nawigacji, musisz wywołać funkcję setViewModelStoreTestNavHostController:

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