Es importante que pruebes la lógica de navegación de tu app antes ponerla a disposición para verificar que tu aplicación funcione de la manera esperada.
El componente Navigation se ocupa de todo el trabajo de administrar la navegación entre los destinos, pasar los argumentos y trabajar con el objeto FragmentManager
.
Como estas capacidades ya se prueban con rigurosidad, no es necesario que vuelvas a probarlas en tu app. Sin embargo, es importante que pruebes las interacciones entre el código específico de la app en tus fragmentos y su NavController
.
En esta guía, se muestran algunos casos de navegación comunes y cómo probarlos.
Cómo probar Navigation de un fragmento
Para probar las interacciones de fragmentos con su NavController
en aislamiento, Navigation 2.3 y las versiones posteriores proporcionan un TestNavHostController
que brinda opciones de APIs para configurar el destino actual y verificar la pila de actividades después de las operaciones de NavController.navigate()
.
Puedes agregar el artefacto de prueba Navigation a tu proyecto si agregas la siguiente dependencia al archivo build.gradle
del módulo de tu 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") }
Supongamos que compilas un juego de preguntas y respuestas. El juego comienza con title_screen y navega a una pantalla in_game cuando el usuario hace clic en Play.
El fragmento que representa title_screen podría verse de la siguiente manera:
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); }); } }
Para probar que la app lleve al usuario sin inconvenientes a la pantalla in_game cuando hace clic en Play, tu prueba debe verificar que este fragmento mueva correctamente el NavController
a la pantalla R.id.in_game
.
Con una combinación del elemento FragmentScenario
, Espresso y TestNavHostController
, puedes recrear las condiciones necesarias para probar esta situación, como se muestra en el siguiente ejemplo:
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); } }
En el ejemplo anterior, se crea una instancia de TestNavHostController
y se la asigna al fragmento. Luego, se utiliza Espresso para controlar la IU y se verifica que se realice la acción de navegación adecuada.
Al igual que con un NavController
real, debes invocar a setGraph
para inicializar el TestNavHostController
. En este ejemplo, el fragmento que se probaba era el destino de inicio de nuestro gráfico. El TestNavHostController
proporciona un método de setCurrentDestination
que te permite configurar el destino actual (y, opcionalmente, los argumentos para ese destino) de modo que el NavController
esté en el estado correcto antes de que comience la prueba.
A diferencia la instancia de NavHostController
que usaría un NavHostFragment
, el TestNavHostController
no activa el comportamiento subyacente de navigate()
(como la FragmentTransaction
que ejecuta FragmentNavigator
) cuando invoques navigate()
, solo se actualizará el estado del TestNavHostController
.
Cómo probar NavigationUI con FragmentScenario
En el ejemplo anterior, la devolución de llamada proporcionada a titleScenario.onFragment()
se llama una vez que el fragmento avanzó en su ciclo de vida al estado RESUMED
. En ese momento, ya se creó y adjuntó la vista del fragmento, de manera que podría ser demasiado tarde en el ciclo de vida para realizar la prueba correctamente. Por ejemplo, cuando usas NavigationUI
con vistas en tu fragmento, por ejemplo, con una Toolbar
controlada por este fragmento, puedes evocar métodos de configuración con tu NavController
antes de que el fragmento llegue al estado RESUMED
. Por lo tanto, necesitas una manera de definir el TestNavHostController
en un momento anterior del ciclo de vida.
Un fragmento que posee su propia Toolbar
se puede escribir de la siguiente manera:
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); } }
En este caso, necesitamos tener el NavController
creado antes de llamar aonViewCreated()
.
Si usas el enfoque anterior de onFragment()
, el elemento TestNavHostController
se establecería demasiado tarde en el ciclo de vida, provocando que la llamada del findNavController()
fallara.
El objeto FragmentScenario
ofrece una interfaz de FragmentFactory
, que se puede utilizar a fin de registrar devoluciones de llamada para eventos del ciclo de vida. Esto también se puede combinar con Fragment.getViewLifecycleOwnerLiveData()
a fin de recibir una devolución de llamada que siga inmediatamente al elemento onCreateView()
, como se muestra en el siguiente ejemplo:
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; } });
Si usas esta técnica, el elemento NavController
estará disponible antes de llamar a onViewCreated()
, lo que permitirá que el fragmento utilice métodos de NavigationUI
sin fallar.
Cómo probar interacciones con entradas de la pila de actividades
Cuando debas interactuar con las entradas de la pila de actividades, TestNavHostController
te permitirá conectar el controlador a tus propios elementos LifecycleOwner
, ViewModelStore
y OnBackPressedDispatcher
de prueba mediante el uso de las API que haya heredado de NavHostController
.
Por ejemplo, cuando pruebes un fragmento que use un ViewModel con alcance de navegación, deberás llamar a setViewModelStore
en el 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())
Temas relacionados
- Crea pruebas de unidades instrumentadas: obtén información sobre cómo configurar tu paquete de pruebas de instrumentación y ejecutar pruebas en un dispositivo Android.
- Espresso: Prueba la IU de tu app con Espresso.
- Prueba de reglas de JUnit4 con AndroidX: Usa las reglas de JUnit4 con las bibliotecas de prueba de AndroidX a fin de brindar más flexibilidad y reducir el código estándar requerido en las pruebas.
- Prueba los fragmentos de tu app: Obtén información para usar
FragmentScenario
a fin de probar los fragmentos de tus apps de manera aislada. - Configura un proyecto para la prueba de AndroidX: Obtén información para declarar las bibliotecas necesarias en los archivos de proyecto de tu app a fin de utilizar la prueba de AndroidX.