È importante testare la logica di navigazione dell'app prima della spedizione per verificare che l'applicazione funzioni come previsto.
Il componente Navigation gestisce tutto il lavoro di gestione della navigazione tra le destinazioni, il passaggio di argomenti e l'utilizzo di FragmentManager. Queste funzionalità sono già
testate rigorosamente, quindi non è necessario testarle di nuovo nella tua app. Ciò che è
importante testare, tuttavia, sono le interazioni tra il codice specifico dell'app
nei tuoi fragment e il relativo NavController.
Test in isolamento
Per testare le interazioni dei fragment con il relativo NavController in isolamento,
Navigation 2.3 e versioni successive forniscono un
TestNavHostController che offre API per
impostare la destinazione corrente e verificare il back stack dopo
le operazioni NavController.navigate().
Puoi aggiungere l'artefatto di test di navigazione al tuo progetto aggiungendo la
seguente dipendenza nel file build.gradle del modulo dell'app:
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") }
Prendi in considerazione un gioco a quiz. Il gioco inizia con una schermata del titolo e passa a una schermata in-game quando l'utente fa clic su Riproduci.
Il frammento che rappresenta title_screen potrebbe avere un aspetto simile a questo:
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 Gioca, il test deve verificare che questo frammento
sposti correttamente NavController alla schermata R.id.in_game.
Utilizzando una combinazione di FragmentScenario, Espresso e
TestNavHostController, puoi ricreare le condizioni necessarie per testare
questo scenario, come mostrato 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 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);
}
}
L'esempio precedente crea un'istanza di TestNavHostController e la assegna
al fragmento. Poi utilizza Espresso per controllare la UI e verifica che venga eseguita l'azione di navigazione appropriata.
Proprio come un vero NavController, devi chiamare setGraph per inizializzare
TestNavHostController. In questo esempio, il frammento in fase di test era la
destinazione iniziale del nostro grafico. TestNavHostController fornisce un metodo setCurrentDestination che consente di impostare la destinazione corrente (e, facoltativamente, gli argomenti per quella destinazione) in modo che NavController sia nello stato corretto prima dell'inizio del test.
A differenza di un'istanza NavHostController che un NavHostFragment utilizzerebbe,
TestNavHostController non attiva il comportamento navigate() sottostante (come FragmentTransaction che FragmentNavigator fa) quando chiami navigate(). Aggiorna solo lo stato di TestNavHostController.
Testare NavigationUI con FragmentScenario
Nell'esempio precedente, il callback fornito a
titleScenario.onFragment() viene chiamato dopo che il fragment è passato attraverso il suo
ciclo di vita allo stato RESUMED. A questo punto, la
visualizzazione del frammento è già stata creata e allegata, quindi potrebbe essere troppo tardi
nel ciclo di vita per eseguire il test correttamente. Ad esempio, quando utilizzi NavigationUI con
le visualizzazioni nel fragment, ad esempio con un Toolbar controllato dal fragment,
puoi chiamare i metodi di configurazione con NavController prima che il fragment
raggiunga lo stato RESUMED. Pertanto, devi trovare un modo per impostare
TestNavHostController in una fase precedente 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 di NavController creato quando viene chiamato onViewCreated(). L'utilizzo dell'approccio precedente di onFragment() imposterebbe il nostro
TestNavHostController troppo tardi nel ciclo di vita, causando l'errore della chiamata
findNavController().
FragmentScenario offre un'interfaccia FragmentFactory
che può essere utilizzata per registrare i callback per gli eventi del ciclo di vita. Questo può
essere combinato con Fragment.getViewLifecycleOwnerLiveData() per ricevere un
callback che segue immediatamente onCreateView(), come mostrato nel 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, NavController è disponibile prima che venga chiamato onViewCreated(), consentendo al fragment di utilizzare i metodi NavigationUI senza arresti anomali.
Testare le interazioni con le voci del back stack
Quando interagisci con le voci del back stack, il
TestNavHostController ti consente di collegare il controller ai tuoi test
LifecycleOwner, ViewModelStore e OnBackPressedDispatcher utilizzando le
API ereditate da NavHostController.
Ad esempio, quando testi un fragment 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à strumentati: scopri come configurare la suite di test strumentati ed eseguire i test su un dispositivo Android.
- Espresso: testa l'interfaccia utente della tua app con Espresso.
- Regole JUnit4 con AndroidX Test: utilizza le regole JUnit 4 con le librerie AndroidX Test per offrire maggiore flessibilità e ridurre il codice boilerplate richiesto nei test.
- Testare i fragment dell'app: scopri come testare i fragment dell'app in isolamento con
FragmentScenario. - Configura il progetto per AndroidX Test: scopri come dichiarare le librerie necessarie nei file di progetto dell'app per utilizzare AndroidX Test.