É importante testar a lógica de navegação do seu app antes de enviá-lo para verificar se ele funciona conforme o esperado.
O componente Navigation lida com todo o trabalho de gerenciamento de navegação entre
destinos, transmissão de argumentos e trabalhos com
FragmentManager
.
Como esses recursos já foram rigorosamente testados, não é necessário testá-los
novamente no 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 o NavController
isoladamente,
o Navigation 2.3 e versões mais recentes fornecem um
TestNavHostController
que oferece APIs para definir o destino atual e verificar a backstack
depois
das operações
NavController.navigate()
.
É possível adicionar o artefato Navigation Testing ao projeto adicionando a
seguinte dependência ao arquivo build.gradle
do módulo do 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") }
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 a tela in_game corretamente quando
ele clica em Play, seu teste precisa verificar se esse fragmento
move corretamente o NavController
para a tela R.id.in_game
.
Usando uma combinação de FragmentScenario
, Espresso
e TestNavHostController
, é possível recriar as condições necessárias para testar
esse cenário, como mostrado no exemplo a seguir:
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); } }
O exemplo acima cria uma instância de TestNavHostController
e a atribui
ao fragmento. Em seguida, ele usa o Espresso para direcionar a IU e verificar se a
ação de navegação adequada foi realizada.
Assim como um NavController
real, você precisa chamar o setGraph
para inicializar
o TestNavHostController
. Nesse exemplo, o fragmento que está sendo testado era
o destino inicial do nosso gráfico. TestNavHostController
fornece uma
setCurrentDestination
que permite definir o destino atual (e, opcionalmente,
argumentos para esse destino) de modo que o NavController
esteja no
estado correto antes do início do teste.
Diferentemente de uma instância NavHostController
que um NavHostFragment
usaria,
TestNavHostController
não aciona o comportamento subjacente navigate()
(como a FragmentTransaction
que o FragmentNavigator
aciona)
quando você chama navigate()
, ele apenas atualiza o estado do
TestNavHostController
.
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
. Nesse ponto, a visualização do fragmento já foi 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 seu
TestNavHostController
no início do ciclo de vida.
Um fragmento que possui o próprio Toolbar
pode ser escrito da seguinte forma:
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); } }
Aqui, precisamos do NavController
criado no momento em que o onViewCreated()
é chamado.
O uso da abordagem anterior de onFragment()
definiria nosso TestNavHostController
muito tarde no ciclo de vida, causando a falha da chamada de findNavController()
.
O 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 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; } });
Usando essa técnica, o NavController
fica disponível antes de
onViewCreated()
ser chamado, permitindo que o fragmento use métodos NavigationUI
sem falhas.
Testar interações com entradas da pilha de retorno
Ao interagir com as entradas da pilha de retorno,
o TestNavHostController
permite que você conecte o controlador a seus próprios
LifecycleOwner
, ViewModelStore
e OnBackPressedDispatcher
de teste usando
as APIs herdadas de
NavHostController
.
Por exemplo, ao testar um fragmento que usa um
ViewModel com escopo de navegação,
é necessário chamar
setViewModelStore
no 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
- Criar testes de unidade de instrumentação: aprenda a configurar seu pacote de testes instrumentados e executar testes em um dispositivo Android.
- Espresso: teste a IU do seu app com o Espresso.
- Regras JUnit4 com o AndroidX Test: use as regras JUnit4 com as bibliotecas do AndroidX Test para oferecer mais flexibilidade e reduzir o código boilerplate exigido nos testes.
- Testar os fragmentos do seu app:
aprenda a testar os fragmentos de apps de forma isolada com
FragmentScenario
. - Configurar projetos para o AndroidX Test: aprenda a declarar as bibliotecas necessárias nos arquivos de projeto do seu app para usar o AndroidX Test.