Participe do evento ⁠#Android11: apresentação de lançamento da versão Beta no dia 3 de junho.

Testar a navegação

É importante testar a lógica de navegação do seu app antes de enviá-lo para verificar se o aplicativo funciona conforme o esperado.

O componente de navegação lida com todo o trabalho de gerenciamento de navegação entre destinos, transmissão de argumentos e trabalho com FragmentManager. Como esses recursos já foram rigorosamente testados, não há necessidade de testá-los novamente no seu app. No entanto, o importante é testar as interações entre o código específico do app nos fragmentos e o respectivo NavController. Este guia apresenta alguns cenários de navegação comuns e como testá-los.

Testar a navegação de fragmentos

Para testar interações de fragmento com NavController isoladamente, você pode fornecer um simulado dentro do seu teste usando o Mockito. Em seguida, você pode usar essa implementação de simulação para verificar as interações com ela.

Digamos que você esteja criando um jogo de curiosidades. O jogo começa com uma title_screen e navega para uma tela in_game quando o usuário clica em "Play".

O fragmento que representa a title_screen pode ter esta aparência:

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 testar se o app direciona o usuário para in_game quando ele clica em Play, seu teste precisa verificar se o fragmento invoca NavController.navigate() com a ação R.id.action_title_screen_to_in_game.

Usando uma combinação de FragmentScenario, Espresso e Mockito, você pode recriar as condições necessárias para testar esse cenário, conforme mostrado neste exemplo:

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

O exemplo acima cria uma instância de simulação de NavController e a atribui ao fragmento. Em seguida, ele usa o Espresso para direcionar a IU e verificar se a ação de navegação apropriada foi realizada.

Testar NavigationUI com FragmentScenario

No exemplo anterior, o callback fornecido para titleScenario.onFragment() é chamado depois que o fragmento passou pelo ciclo de vida para o estado RESUMED. Naquele ponto, a visualização do fragmento já tinha sido criada e anexada. Por isso, pode ser muito tarde no ciclo de vida para ser testada corretamente. Por exemplo, ao usar NavigationUI com visualizações no fragmento, como com um Toolbar controlado pelo fragmento, você pode chamar métodos de configuração com NavController antes que o fragmento alcance o estado RESUMED. Assim, você precisa definir sua simulação de NavController no início do ciclo de vida.

Um fragmento que possui o próprio Toolbar pode ser gravado da seguinte forma:

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

Aqui, precisamos que NavController seja simulado no momento em que onViewCreated() é chamado. O uso da abordagem anterior de onFragment() definiria nossa simulação NavController muito tarde no ciclo de vida, causando falha da chamada findNavController().

FragmentScenario oferece uma interface FragmentFactory que pode ser usada para registrar callbacks para eventos de ciclo de vida. Isso pode ser combinado com Fragment.getViewLifecycleOwnerLiveData() para receber um callback imediatamente após onCreateView(), conforme mostrado neste exemplo:

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

Usando essa técnica, o NavController fica disponível antes de onViewCreated() ser chamado, permitindo que o fragmento use métodos NavigationUI sem falhas.