Únete a ⁠ #Android11: The Beta Launch Show el 3 de junio.

Cómo probar Navigation

Es importante que pruebes la lógica de navegación de tu app antes ponerla a disposición a fin de 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 rigurosamente, no es necesario que vuelvas a hacerlo 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

Si deseas probar las interacciones de fragmentos con su NavController de manera aislada, puedes usar Mockito para proporcionar un ficticio dentro de tu prueba. Luego, puedes usar esa implementación ficticia para verificar las interacciones con ella.

Supongamos que estás creando 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 correctamente al usuario a la pantalla in_game cuando hace clic en Play, tu prueba debe verificar que este fragmento invoque a NavController.navigate() con la acción R.id.action_title_screen_to_in_game.

Si usas una combinación del elemento FragmentScenario, Espresso y Mockito, 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 mock NavController
            val mockNavController = mock(NavController::class.java)

            // Create a graphical FragmentScenario for the TitleScreen
            val titleScenario = launchFragmentInContainer<TitleScreen>()

            // Set the NavController property on the fragment
            titleScenario.onFragment { fragment ->
                Navigation.setViewNavController(fragment.requireView(), mockNavController)
            }

            // Verify that performing a click prompts the correct Navigation action
            onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click())
            verify(mockNavController).navigate(R.id.action_title_screen_to_in_game)
        }
    }
    

Java

    @RunWith(AndroidJUnit4.class)
    public class TitleScreenTestJava {

        @Test
        public void testNavigationToInGameScreen() {

            // Create a mock NavController
            NavController mockNavController = mock(NavController.class);

            // Create a graphical FragmentScenario for the TitleScreen
            FragmentScenario<TitleScreen> titleScenario = FragmentScenario.launchInContainer(TitleScreen.class);

            // Set the NavController property on the fragment
            titleScenario.onFragment(fragment ->
                    Navigation.setViewNavController(fragment.requireView(), mockNavController)
            );

            // Verify that performing a click prompts the correct Navigation action
            onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click());
            verify(mockNavController).navigate(R.id.action_title_screen_to_in_game);
        }
    }
    

El ejemplo anterior crea una instancia ficticia de NavController y la asigna al fragmento. Luego, utiliza Espresso para controlar la IU y verifica que se realice la acción de navegación adecuada.

Cómo probar NavigationUI con FragmentScenario

En el ejemplo anterior, se llama a la devolución de llamada proporcionada para titleScenario.onFragment() una vez que el fragmento avanzó en su ciclo de vida al estado RESUMED. Para entonces, 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, como con un objeto Toolbar controlado por tu fragmento, puedes llamar a métodos de configuración con tu elemento NavController antes de que el fragmento llegue al estado RESUMED. Por lo tanto, necesitas una manera de definir tu elemento NavController ficticio antes en el ciclo de vida.

Un fragmento con su propio objeto Toolbar se puede escribir de la siguiente manera:

Kotlin

    class TitleScreen : Fragment() {

        override fun onCreateView(
                inflater: LayoutInflater,
                container: ViewGroup?,
                savedInstanceState: Bundle?
        ) = return inflater.inflate(R.layout.fragment_title_screen, container, false)

        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 {

        @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) {
            NavController navController = Navigation.findNavController(view);
            view.findViewById(R.id.toolbar).setupWithNavController(navController);
        }
    }
    

En este caso, necesitamos el elemento NavController ficticio para el momento en el que se llama a onViewCreated(). Si usas el enfoque anterior de onFragment(), el elemento NavController ficticio se definiría demasiado tarde en el ciclo de vida y la llamada a findNavController() fallaría.

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. También se puede combinar con Fragment.getViewLifecycleOwnerLiveData() a fin de recibir una devolución de llamada que siga inmediatamente a 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 mock NavController
            fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
                if (viewLifecycleOwner != null) {
                    // The fragment’s view has just been created
                    Navigation.setViewNavController(fragment.requireView(), mockNavController)
                }
            }
        }
    }
    

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 mock NavController
            titleScreen.getViewLifecycleOwnerLiveData().observeForever(new Observer<LifecycleOwner>() {
                @Override
                public void onChanged(LifecycleOwner viewLifecycleOwner) {

                    // The fragment’s view has just been created
                    if (viewLifecycleOwner != null) {
                        Navigation.setViewNavController(titleScreen.requireView(), mockNavController);
                    }

                }
            });
            return titleScreen;
        }
    });
    

Si usas esta técnica, el elemento NavController estará disponible antes de llamar a onViewCreated(), lo que permite que el fragmento utilice métodos NavigationUI sin fallar.