Menguji navigasi

Sebelum mengirimkan logika navigasi aplikasi, Anda harus mengujinya terlebih dahulu agar aplikasi dapat berfungsi seperti yang diharapkan.

Komponen Navigasi menangani semua tugas mengelola navigasi antar-tujuan, meneruskan argumen, dan bekerja dengan FragmentManager. Kemampuan ini sudah diuji secara ketat, sehingga Anda tidak perlu mengujinya kembali. Tetapi, yang perlu untuk diuji adalah interaksi antara kode khusus aplikasi dalam fragmen Anda dan NavController-nya. Panduan ini membahas beberapa skenario navigasi umum beserta cara mengujinya.

Menguji navigasi fragmen

Untuk menguji interaksi fragmen dengan NavController-nya secara terpisah, Navigasi 2.3 dan yang lebih baru menyediakan TestNavHostController yang memberikan API untuk menyetel tujuan saat ini dan memverifikasi data sebelumnya setelah operasi NavController.navigate().

Anda dapat menambahkan artefak Pengujian Navigasi ke project dengan menambahkan dependensi berikut dalam file build.gradle modul aplikasi Anda:

Groovy

dependencies {
  def nav_version = "2.8.5"

  androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
}

Kotlin

dependencies {
  val nav_version = "2.8.5"

  androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
}

Misalnya, Anda membuat game trivia. Game dimulai dengan title_screen menavigasi ke layar in_game saat pengguna mengklik play.

Fragmen yang mewakili title_screen mungkin akan terlihat seperti berikut:

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

Untuk menguji apakah aplikasi menavigasi pengguna dengan benar ke layar in_game saat pengguna mengklik Play, pengujian Anda harus memverifikasi bahwa fragmen ini telah memindahkan NavController ke layar R.id.in_game.

Dengan menggunakan kombinasi FragmentScenario, Espresso, dan TestNavHostController, Anda dapat membuat ulang kondisi yang diperlukan untuk menguji skenario ini, seperti ditunjukkan dalam contoh berikut:

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

Contoh di atas membuat instance TestNavHostController dan menetapkannya ke fragmen. Kemudian, Espresso akan digunakan untuk menjalankan UI dan memverifikasi bahwa tindakan navigasi yang sesuai telah dilakukan.

Seperti halnya NavController yang sebenarnya, Anda harus memanggil setGraph untuk menginisialisasi TestNavHostController. Dalam contoh ini, fragmen yang diuji adalah tujuan awal dari grafik kami. TestNavHostController menyediakan setCurrentDestination yang memungkinkan Anda menetapkan tujuan saat ini (dan secara opsional, argumen untuk tujuan tersebut) sehingga NavController dalam status yang benar sebelum dimulai pengujian.

Tidak seperti instance NavHostController yang akan digunakan oleh NavHostFragment, TestNavHostController tidak memicu perilaku navigate() yang mendasarinya (seperti FragmentTransaction yang dilakukan FragmentNavigator) saat Anda memanggil navigate() - ini hanya memperbarui status TestNavHostController.

Menguji NavigationUI dengan FragmentScenario

Pada contoh sebelumnya, callback yang diberikan ke titleScenario.onFragment() akan dipanggil setelah fragmen dipindahkan melalui siklus prosesnya ke status RESUMED. Pada tahap ini, tampilan fragmen telah dibuat dan dilampirkan, sehingga mungkin sudah terlambat untuk diuji dengan benar dalam siklus proses. Misalnya, saat menggunakan NavigationUI dengan tampilan dalam fragmen Anda, seperti dengan Toolbar yang dikontrol oleh fragmen, Anda dapat memanggil metode penyiapan dengan NavController sebelum fragmen tersebut mencapai status RESUMED. Dengan demikian, Anda membutuhkan cara untuk menetapkan TestNavHostController lebih awal dalam siklus proses.

Fragmen yang memiliki Toolbar-nya sendiri dapat ditulis sebagai berikut:

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

Dalam hal ini, kita perlu membuat NavController pada saat onViewCreated() dipanggil. Menggunakan pendekatan onFragment() sebelumnya akan menetapkan TestNavHostController terlalu lambat pada siklus proses, yang menyebabkan panggilan findNavController() gagal.

FragmentScenario memberikan antarmuka FragmentFactory yang dapat digunakan untuk mendaftarkan callback untuk peristiwa siklus proses. Hal ini dapat dikombinasikan dengan Fragment.getViewLifecycleOwnerLiveData() untuk menerima callback yang segera mengikuti onCreateView(), seperti yang ditunjukkan pada contoh berikut:

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

Dengan menggunakan teknik ini, NavController akan tersedia sebelum onViewCreated() dipanggil, yang memungkinkan fragmen menggunakan metode NavigationUI tanpa error.

Menguji interaksi dengan entri data sebelumnya

Saat berinteraksi dengan entri data sebelumnya, TestNavHostController memungkinkan Anda menghubungkan pengontrol ke LifecycleOwner, ViewModelStore, dan OnBackPressedDispatcher pengujian Anda sendiri dengan menggunakan API yang diwarisi dari NavHostController.

Misalnya, saat menguji fragmen yang menggunakan ViewModel cakupan navigasi, Anda harus memanggil setViewModelStore di 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())