Przetestuj nawigację

Przed wysyłką należy przetestować logikę nawigacji w aplikacji, aby sprawdzić, czy działa ona zgodnie z oczekiwaniami.

Komponent Nawigacja obsługuje wszystkie zadania związane z zarządzaniem nawigacją miejsc docelowych, przekazywania argumentów oraz pracy z FragmentManager Te funkcje zostały już dokładnie przetestowane, więc nie trzeba testować je ponownie w aplikacji. Ważne jest jednak, aby przetestować interakcje między kodem aplikacji we fragmentach a ich NavController W tym przewodniku omawiamy kilka typowych scenariuszy nawigacji i sposoby ich testowania.

Nawigacja po fragmentach testowych

Aby przetestować osobno interakcje z fragmentami za pomocą NavController, Nawigacja w wersji 2.3 i nowszych TestNavHostController. udostępnia interfejsy API do ustawiania bieżącego miejsca docelowego i weryfikowania tylnej części stos po NavController.navigate() operacji.

Możesz dodać do projektu artefakt Test nawigacji, dodając do niego ta zależność w pliku build.gradle modułu aplikacji:

Odlotowe

dependencies {
  def nav_version = "2.8.0"

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

Kotlin

dependencies {
  val nav_version = "2.8.0"

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

Załóżmy, że tworzysz quiz. Gra zaczyna się od title_screen i przechodzi do ekranu in_game, gdy użytkownik kliknie reklamę Google Play.

Fragment reprezentujący parametr 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 przetestować, czy aplikacja prawidłowo przechodzi użytkownika na ekran in_game, gdy: użytkownik kliknie Odtwórz, test musi sprawdzić, czy fragment prawidłowo przenosi element NavController na ekran R.id.in_game.

Korzystając z połączenia FragmentScenario, Espresso, i TestNavHostController, możesz odtworzyć warunki niezbędne do przetestowania w tym scenariuszu, jak w przykładzie poniżej:

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

Ten przykład tworzy i przypisuje instancję TestNavHostController do tego fragmentu. Następnie narzędzie wykorzystuje interfejs Espresso do obsługi interfejsu i sprawdza, czy podjęto odpowiednie działanie.

Tak jak w przypadku prawdziwego NavController, musisz wywołać setGraph, aby zainicjować TestNavHostController. W tym przykładzie testowany fragment to jest to miejsce, w którym zaczyna się nasz wykres. TestNavHostController udostępnia setCurrentDestination. która pozwala ustawić bieżące miejsce docelowe (oraz opcjonalnie argumentów dla tego miejsca docelowego), tak aby NavController znalazł się w właściwy stan przed rozpoczęciem testu.

W przeciwieństwie do instancji NavHostController, której używa NavHostFragment, TestNavHostController nie aktywuje bazowego elementu navigate() działanie (takie jak FragmentTransaction, które wykonuje FragmentNavigator) gdy wywołujesz navigate() — aktualizuje tylko stan TestNavHostController

Testowanie interfejsu NavigationUI z użyciem fragmentu FragmentScenariusz

W poprzednim przykładzie wywołanie zwrotne zostało udostępnione do titleScenario.onFragment() jest wywoływana po przejściu fragmentu przez swój cykl życia do RESUMED. stanu. Na tym etapie widok fragmentu został już utworzony i dołączony, więc może być jeszcze za późno w cyklu życia na prawidłowe testowanie. Na przykład podczas używania funkcji NavigationUI z wyświetleniami we fragmencie, np. z kontrolowanym elementem Toolbar według swojego fragmentu, możesz wywołać metody konfiguracji za pomocą NavController przed fragment osiągnie stan RESUMED. Potrzebujesz więc sposobu TestNavHostController na wcześniejszym etapie cyklu życia.

Fragment zawierający własny element Toolbar może być zapisany w taki 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);
    }
}

Tutaj potrzebujemy elementu NavController utworzonego przed wywołaniem funkcji onViewCreated(). Zastosowanie poprzedniego podejścia, czyli onFragment(), sprawiłoby, że TestNavHostController zbyt późno w cyklu życia, przez co wywołanie findNavController() kończy się niepowodzeniem.

FragmentScenario oferuje FragmentFactory. który może służyć do rejestrowania wywołań zwrotnych dla zdarzeń cyklu życia. Może to spowodować zostanie połączona z Fragment.getViewLifecycleOwnerLiveData(), aby otrzymać wywołanie zwrotne następujące bezpośrednio po onCreateView(), jak poniżej przykład:

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 interfejs NavController jest dostępny przed Wywoływana jest metoda onViewCreated(), która pozwala na użycie metod NavigationUI dla fragmentu bez awarii.

Testowanie interakcji z wpisami stosu wstecznego

Podczas interakcji z wpisami wstecznymi, TestNavHostController umożliwia podłączenie kontrolera do własnego test LifecycleOwner, ViewModelStore i OnBackPressedDispatcher przez przy użyciu interfejsów API odziedziczonych NavHostController.

Na przykład podczas testowania fragmentu z atrybutem ViewModel o zakresie do nawigacji, Musisz zadzwonić setViewModelStore. – 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())