Zanim opublikujesz aplikację, przetestuj logikę nawigacji, aby sprawdzić, czy działa ona zgodnie z oczekiwaniami.
Komponent Navigation zajmuje się zarządzaniem nawigacją między miejscami docelowymi, przekazywaniem argumentów i współpracą z FragmentManager. Te funkcje są już dokładnie przetestowane, więc nie musisz ich ponownie testować w aplikacji. Ważne jest jednak, aby przetestować interakcje między kodem specyficznym dla aplikacji w fragmentach a ich NavController.
Testowanie w izolacji
Aby przetestować interakcje fragmentów w izolacji, biblioteka NavController w wersji 2.3 i nowszych udostępnia klasę TestNavHostController, która zawiera interfejsy API do ustawiania bieżącego miejsca docelowego i weryfikowania listy wstecznej po operacjach NavController.navigate().
Aby dodać artefakt testowania nawigacji do projektu, dodaj tę zależność w pliku build.gradle modułu aplikacji:
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") }
Zagraj w quiz. Gra zaczyna się od ekranu tytułowego, a gdy użytkownik kliknie przycisk odtwarzania, przechodzi do ekranu w grze.
Fragment reprezentujący title_screen może wyglądać tak:
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);
});
}
}
Aby sprawdzić, czy aplikacja prawidłowo przenosi użytkownika na ekran in_game, gdy kliknie on Graj, test musi zweryfikować, czy ten fragment prawidłowo przenosi NavController na ekran R.id.in_game.
Używając kombinacji FragmentScenario, Espresso i TestNavHostController, możesz odtworzyć warunki niezbędne do przetestowania tego scenariusza, jak pokazano w tym przykładzie:
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);
}
}
W poprzednim przykładzie tworzona jest instancja klasy TestNavHostController i przypisywana do fragmentu. Następnie używa Espresso do sterowania interfejsem i sprawdza, czy wykonano odpowiednie działanie nawigacyjne.
Podobnie jak w przypadku prawdziwego NavController, musisz wywołać setGraph, aby zainicjować TestNavHostController. W tym przykładzie testowany fragment to miejsce docelowe na początku naszego wykresu. TestNavHostController udostępnia metodę setCurrentDestination, która pozwala ustawić bieżące miejsce docelowe (i opcjonalnie argumenty dla tego miejsca docelowego), aby przed rozpoczęciem testu NavController znajdował się w odpowiednim stanie.
W przeciwieństwie do NavHostController, z którego korzysta NavHostFragment, wywołanie TestNavHostController nie powoduje uruchomienia bazowego zachowania navigate() (np. FragmentTransaction, które jest uruchamiane przez FragmentNavigator). Wywołanie navigate() powoduje tylko aktualizację stanu TestNavHostController.
Testowanie NavigationUI za pomocą FragmentScenario
W powyższym przykładzie wywołanie zwrotne przekazane do funkcji
titleScenario.onFragment() jest wywoływane po przejściu fragmentu przez cykl życia do stanu RESUMED. W tym czasie widok fragmentu jest już utworzony i dołączony, więc może być za późno na prawidłowe przetestowanie go w cyklu życia. Na przykład podczas korzystania z NavigationUI z widokami w fragmencie, np. z Toolbar kontrolowanym przez fragment, możesz wywoływać metody konfiguracji za pomocą NavController, zanim fragment osiągnie stan RESUMED. Dlatego musisz mieć możliwość ustawienia wartości parametru
TestNavHostController na wcześniejszym etapie cyklu życia.
Fragment, który ma własny Toolbar, można zapisać w ten sposób:
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);
}
}
W tym miejscu potrzebujemy NavController utworzonego do czasu wywołania onViewCreated(). Użycie poprzedniego podejścia onFragment() spowodowałoby ustawienie TestNavHostController zbyt późno w cyklu życia, co spowodowałoby niepowodzenie wywołania findNavController().
FragmentScenario udostępnia interfejs FragmentFactory, który umożliwia rejestrowanie wywołań zwrotnych dla zdarzeń cyklu życia. Możesz połączyć tę funkcję z parametrem Fragment.getViewLifecycleOwnerLiveData(), aby otrzymać wywołanie zwrotne natychmiast po onCreateView(), jak pokazano w tym przykładzie:
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;
}
});
Dzięki tej technice obiekt NavController jest dostępny przed wywołaniem funkcji onViewCreated(), co pozwala fragmentowi używać metod NavigationUI bez powodowania awarii.
Testowanie interakcji z wpisami na stosie wstecznym
Podczas interakcji z elementami listy wstecznej element TestNavHostController umożliwia połączenie kontrolera z własnymi testami LifecycleOwner, ViewModelStore i OnBackPressedDispatcher za pomocą interfejsów API odziedziczonych z NavHostController.
Jeśli na przykład testujesz fragment, który korzysta z obiektu ViewModel o zakresie nawigacji, musisz wywołać funkcję setViewModelStore w 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())
Powiązane artykuły
- Tworzenie testów jednostkowych z instrumentacją – dowiedz się, jak skonfigurować zestaw testów z instrumentacją i uruchamiać testy na urządzeniu z Androidem.
- Espresso – testuj interfejs aplikacji za pomocą Espresso.
- Reguły JUnit4 z AndroidX Test – używaj reguł JUnit 4 z bibliotekami AndroidX Test, aby zwiększyć elastyczność i zmniejszyć ilość powtarzalnego kodu wymaganego w testach.
- Testowanie fragmentów aplikacji – dowiedz się, jak testować fragmenty aplikacji w izolacji za pomocą
FragmentScenario. - Konfigurowanie projektu na potrzeby AndroidX Test – dowiedz się, jak zadeklarować potrzebne biblioteki w plikach projektu aplikacji, aby używać AndroidX Test.