È 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())
Argomenti correlati
- Crea test delle unità strumentate: scopri come configurare la tua suite di test strumentata ed eseguire test su un dispositivo Android.
- Espresso: testa l'UI della tua app con Espresso.
- Regole JUnit4 con AndroidX Test: utilizza le regole JUnit 4 con le librerie di test AndroidX per offrire maggiore flessibilità e ridurre il codice boilerplate richiesto nei test.
- Testa i frammenti della tua app:
scopri come testare i frammenti dell'app in modo isolato con
FragmentScenario
. - Configura progetto per AndroidX Test: scopri come dichiarare le librerie necessarie nei file di progetto della tua app per utilizzare AndroidX Test.