Testa la navigazione

È importante testare la logica di navigazione dell'app prima di spedirla per verificare che l'app funzioni come previsto.

Il componente Navigazione gestisce tutte le attività di gestione della navigazione tra destinazioni, passare argomenti e lavorare con FragmentManager Queste funzionalità sono già rigorosamente testate, quindi non è necessario testare li di nuovo nell'app. È importante verificare, tuttavia, che le interazioni tra il codice specifico dell'app nei tuoi frammenti e i relativi NavController Questa guida illustra alcuni scenari di navigazione comuni e spiega come testarli.

Navigazione dei frammenti di test

Per testare le interazioni dei frammenti con il relativo NavController in isolamento, La versione 2.3 e le versioni successive offrono un TestNavHostController che fornisce API per impostare la destinazione attuale e verificare il pila dopo NavController.navigate() operazioni.

Puoi aggiungere l'artefatto del test di navigazione al progetto aggiungendo l'elemento seguente dipendenza nel file build.gradle del modulo dell'app:

Groovy

dependencies {
  def nav_version = "2.8.4"

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

Kotlin

dependencies {
  val nav_version = "2.8.4"

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

Supponiamo che tu stia creando un gioco a quiz. Il gioco inizia con title_screen e passa a una schermata in_game quando l'utente fa clic riprodurre.

Il frammento che rappresenta title_screen potrebbe avere un aspetto simile al seguente:

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

Per verificare che l'app indirizzi correttamente l'utente alla schermata in_game quando l'utente fa clic su Riproduci, il test deve verificare che questo frammento sposta correttamente NavController nella schermata R.id.in_game.

Utilizzando una combinazione di FragmentScenario, Espresso, e TestNavHostController, puoi ricreare le condizioni necessarie per questo scenario, come illustrato nell'esempio seguente:

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

L'esempio precedente crea un'istanza di TestNavHostController e la assegna al frammento. Quindi utilizza Espresso per gestire l'UI e verifica che venga intrapresa l'azione di navigazione appropriata.

Proprio come un NavController reale, devi chiamare setGraph per inizializzare TestNavHostController. In questo esempio, il frammento sottoposto a test è stato la destinazione iniziale del grafico. TestNavHostController offre un setCurrentDestination che ti consente di impostare la destinazione corrente (e, facoltativamente, per quella destinazione) in modo che NavController sia nell' lo stato corretto prima di iniziare il test.

A differenza di un'istanza NavHostController utilizzata da un NavHostFragment, TestNavHostController non attiva navigate() sottostante (ad esempio i FragmentTransaction di FragmentNavigator) quando chiami navigate(), l'aggiornamento aggiorna solo lo stato del TestNavHostController.

Testa l'interfaccia utente di navigazione con lo scenario Fragment

Nell'esempio precedente, il callback fornito a titleScenario.onFragment() viene chiamato dopo che il frammento ha completato il proprio ciclo di vita RESUMED stato. A questo punto la vista del frammento è già stata creata e allegata, quindi potrebbe essere troppo tardi nel ciclo di vita per eseguire correttamente i test. Ad esempio, quando utilizzi NavigationUI con visualizzazioni nel frammento, ad esempio con un elemento controllato da Toolbar dal frammento, puoi richiamare i metodi di configurazione con NavController prima il frammento raggiunge lo stato RESUMED. È quindi necessario un modo per impostare TestNavHostController nelle prime fasi del ciclo di vita.

Un frammento proprietario di un proprio Toolbar può essere scritto come segue:

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

Qui ci serve il NavController creato quando verrà chiamato onViewCreated(). L'utilizzo dell'approccio precedente di onFragment() imposterebbe le nostre TestNavHostController troppo tardi nel ciclo di vita, causando l'esito negativo della chiamata findNavController().

FragmentScenario offre un FragmentFactory che può essere utilizzata per registrare callback per eventi del ciclo di vita. Questo può da combinare con Fragment.getViewLifecycleOwnerLiveData() per ricevere un che segue immediatamente onCreateView(), come illustrato nell'esempio seguente esempio:

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

Utilizzando questa tecnica, il NavController è disponibile prima Viene chiamato il metodo onViewCreated(), consentendo al frammento di usare metodi NavigationUI senza arresti anomali.

Test delle interazioni con voci back stack

Quando interagisci con le voci back stack, il TestNavHostController ti permette di collegare il controller al tuo il test LifecycleOwner, ViewModelStore e OnBackPressedDispatcher di utilizzando le API da cui eredita NavHostController

Ad esempio, quando esegui il test di un frammento che utilizza un ViewModel con ambito di navigazione, devi chiamare setViewModelStore: il 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())
  • Creare test delle unità instrumentati - Scopri come configurare la tua suite di test instrumentata ed eseguire test su un Android dispositivo.
  • Espresso. Testa l'interfaccia utente della tua app. con Espresso.
  • Regole JUnit4 con AndroidX Test: utilizza JUnit 4 con le librerie AndroidX Test per offrire maggiore flessibilità e ridurre il codice boilerplate richiesto nei test.
  • Testa i frammenti dell'app: Scopri come testare i frammenti di app in modo isolato con FragmentScenario.
  • Set up project for AndroidX Test (Configura il progetto per il test AndroidX) - Ulteriori informazioni come dichiarare le librerie necessarie nei file di progetto della tua app per usare AndroidX Testa.