Testa la navigazione

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

Il componente Navigazione gestisce tutto il lavoro di gestione della navigazione tra le destinazioni, il passaggio di argomenti e l'utilizzo della FragmentManager. Queste funzionalità sono già rigorosamente testate, quindi non è necessario testarle di nuovo nell'app. Ciò che è importante testare, tuttavia, sono le interazioni tra il codice specifico dell'app nei tuoi frammenti e il relativo NavController. Questa guida illustra alcuni scenari di navigazione comuni e come testarli.

Testa la navigazione dei frammenti

Per testare le interazioni con i frammenti con i rispettivi NavController in modo isolato, Navigazione 2.3 e versioni successive fornisce un elemento TestNavHostController che fornisce le API per impostare la destinazione attuale e verificare lo stack posteriore dopo le operazioni di NavController.navigate().

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

Trendy

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

Supponiamo che tu stia creando un gioco di cultura generale. Il gioco inizia con un title_screen e passa a una schermata in_game quando l'utente fa clic sul pulsante di gioco.

Il frammento che rappresenta title_screen potrebbe avere il seguente aspetto:

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 rimandi correttamente l'utente alla schermata in_game quando l'utente fa clic su Gioca, il test deve verificare che questo frammento sposti correttamente NavController nella schermata R.id.in_game.

Utilizzando una combinazione di FragmentScenario, Espresso e TestNavHostController, puoi ricreare le condizioni necessarie per testare 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 sopra crea un'istanza di TestNavHostController e la assegna al frammento. Quindi utilizza Espresso per guidare l'interfaccia utente e verificare che venga intrapresa l'azione di navigazione appropriata.

Come un NavController reale, devi chiamare setGraph per inizializzare TestNavHostController. In questo esempio, il frammento testato è stata la destinazione iniziale del grafico. TestNavHostController fornisce un metodo setCurrentDestination che consente di impostare la destinazione corrente (e, facoltativamente, argomenti per quella destinazione) in modo che NavController sia nello stato corretto prima dell'inizio del test.

A differenza di un'istanza NavHostController che potrebbe utilizzare un NavHostFragment, TestNavHostController non attiva il comportamento navigate() sottostante (ad esempio l'FragmentTransaction che FragmentNavigator fa) quando chiami navigate(), ma aggiorna solo lo stato di TestNavHostController.

Testa l'interfaccia utente di navigazione con scenario di frammento

Nell'esempio precedente, il callback fornito a titleScenario.onFragment() viene chiamato dopo che il frammento è passato allo stato RESUMED durante il suo ciclo di vita. A questo punto, la vista del frammento è già stata creata e collegata, pertanto potrebbe essere troppo tardi nel ciclo di vita per eseguire correttamente i test. Ad esempio, quando utilizzi NavigationUI con viste nel frammento, ad esempio con un Toolbar controllato dal frammento, puoi chiamare i metodi di configurazione con il tuo NavController prima che il frammento raggiunga lo stato RESUMED. Di conseguenza, è necessario un modo per impostare TestNavHostController nelle prime fasi del ciclo di vita.

Un frammento che possiede il 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 abbiamo bisogno del NavController creato quando viene chiamato onViewCreated(). L'utilizzo dell'approccio precedente di onFragment() imposterebbe TestNavHostController troppo tardi nel ciclo di vita, causando un errore della chiamata findNavController().

FragmentScenario offre un'interfaccia FragmentFactory che può essere utilizzata per registrare callback per gli eventi del ciclo di vita. Questa operazione può essere combinata con Fragment.getViewLifecycleOwnerLiveData() per ricevere un callback che segue immediatamente onCreateView(), come mostrato nell'esempio seguente:

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, NavController è disponibile prima della chiamata onViewCreated(), consentendo al frammento di utilizzare i metodi NavigationUI senza causare arresti anomali.

Test delle interazioni con le voci back stack

Durante l'interazione con le voci dello stack di back, TestNavHostController ti consente di connettere il controller ai tuoi test LifecycleOwner, ViewModelStore e OnBackPressedDispatcher utilizzando le API che eredita da NavHostController.

Ad esempio, quando testi un frammento che utilizza un ViewModel con ambito di navigazione, devi chiamare setViewModelStore su 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())