É 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 o
FragmentManager. Como esses recursos já foram
rigorosamente testados, não há necessidade de testá-los novamente no seu app. No entanto, o que é
importante testar são as interações entre o código específico do app
nos fragmentos e o respectivo NavController.
Testar de forma isolada
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.9.8" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.8" androidTestImplementation("androidx.navigation:navigation-testing:$nav_version") }
Considere 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 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);
}
}
O exemplo anterior 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 um
setCurrentDestination método 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 do NavHostController que um NavHostFragment usaria, o TestNavHostController não aciona o comportamento navigate() subjacente (como a FragmentTransaction que o FragmentNavigator aciona) quando você chama o método 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().
FragmentScenario oferece uma FragmentFactory
interface 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 backstack, 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())
Tópicos relacionados
- Criar testes de unidade instrumentados: saiba como configurar seu conjunto 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 o projeto para o teste do AndroidX: aprenda a declarar as bibliotecas necessárias nos arquivos de projeto do seu app para usar o teste do AndroidX.