Parçada gezinmeyi test etme

Uygulamanızın beklediğiniz gibi çalıştığını doğrulamak için göndermeden önce uygulamanızın gezinme mantığını test etmeniz önemlidir.

Navigation bileşeni, hedefler arasında gezinmeyi yönetme, bağımsız değişkenleri iletme ve FragmentManager ile çalışma gibi tüm işlemleri gerçekleştirir. Bu özellikler zaten titizlikle test edildiğinden uygulamanızda tekrar test etmenize gerek yoktur. Ancak parçalarınızdaki uygulamaya özel kod ile NavController arasındaki etkileşimleri test etmeniz önemlidir.

Yalıtılmış ortamda test etme

Parça etkileşimlerini NavController ile izole bir şekilde test etmek için, Navigation 2.3 ve sonraki sürümlerde, geçerli hedefi ayarlamak ve NavController.navigate() işlemlerinden sonra geri yığını doğrulamak için API'ler sağlayan bir TestNavHostController bulunur.

Aşağıdaki bağımlılığı uygulama modülünüzün build.gradle dosyasına ekleyerek Navigation Testing yapısını projenize ekleyebilirsiniz:

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

Bilgi yarışması oyunu oynatın. Oyun, title_screen ile başlar ve kullanıcı oynat'ı tıkladığında in_game ekranına gider.

Bilgi yarışması oyunu gezinme akışı
Şekil 1. Bilgi yarışması oyunu gezinme akışı.

title_screen öğesini temsil eden parça şöyle görünebilir:

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

Kullanıcı Oyna'yı tıkladığında uygulamanın kullanıcıyı in_game ekranına doğru şekilde yönlendirdiğini test etmek için testinizin bu parçanın NavController öğesini R.id.in_game ekranına doğru şekilde taşıdığını doğrulaması gerekir.

Aşağıdaki örnekte gösterildiği gibi FragmentScenario, Espresso ve TestNavHostController kombinasyonunu kullanarak bu senaryoyu test etmek için gereken koşulları yeniden oluşturabilirsiniz:

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

Yukarıdaki örnekte TestNavHostController öğesinin bir örneği oluşturulur ve bu örnek, parçaya atanır. Ardından, kullanıcı arayüzünü yönlendirmek için Espresso'yu kullanır ve uygun gezinme işleminin yapıldığını doğrular.

Gerçek bir NavController gibi, TestNavHostController öğesini başlatmak için setGraph öğesini çağırmanız gerekir. Bu örnekte, test edilen parça grafiğimizin başlangıç hedefiydi. TestNavHostController, mevcut hedefi (ve isteğe bağlı olarak bu hedefe yönelik bağımsız değişkenleri) ayarlamanıza olanak tanıyan bir setCurrentDestination yöntemi sunar. Böylece, testiniz başlamadan önce NavController doğru durumda olur.

NavHostFragment tarafından kullanılan bir NavHostController örneğinin aksine, TestNavHostController, navigate()'ı çağırdığınızda temel navigate() davranışını (ör. FragmentNavigator'ün yaptığı gibi FragmentTransaction) tetiklemez. Yalnızca TestNavHostController'ın durumunu günceller.

NavigationUI'yı FragmentScenario ile test etme

Önceki örnekte, titleScenario.onFragment() öğesine sağlanan geri çağırma, parça yaşam döngüsünde RESUMED durumuna geçtikten sonra çağrılır. Bu noktada, parçanın görünümü zaten oluşturulmuş ve eklenmiş olduğundan, yaşam döngüsünde düzgün bir şekilde test etmek için çok geç olabilir. Örneğin, parçanızdaki görünümlerle NavigationUI kullanırken (ör. parçanız tarafından kontrol edilen bir Toolbar ile) parça RESUMED durumuna ulaşmadan önce NavController ile kurulum yöntemlerini çağırabilirsiniz. Bu nedenle, yaşam döngüsünde TestNavHostController değerini daha erken ayarlamanız gerekir.

Kendi Toolbar öğesine sahip bir parça aşağıdaki gibi yazılabilir:

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

Burada, onViewCreated() çağrıldığında NavController oluşturulmuş olmalıdır. Önceki onFragment() yaklaşımını kullanmak, TestNavHostController öğemizi yaşam döngüsünde çok geç ayarlayarak findNavController() çağrısının başarısız olmasına neden olur.

FragmentScenario, yaşam döngüsü etkinlikleri için geri çağırmaları kaydetmek üzere kullanılabilecek bir FragmentFactory arayüzü sunar. Bu, aşağıdaki örnekte gösterildiği gibi Fragment.getViewLifecycleOwnerLiveData() ile birleştirilerek onCreateView()'den hemen sonra geri arama alınabilir:

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

Bu teknik kullanıldığında NavController, onViewCreated() çağrılmadan önce kullanılabilir. Böylece, parça NavigationUI yöntemlerini kilitlenmeden kullanabilir.

Geri yığın girişleriyle etkileşimleri test etme

eski yığın girişleriyle etkileşimde bulunurken TestNavHostController, NavHostController'dan devraldığı API'leri kullanarak denetleyiciyi kendi test LifecycleOwner, ViewModelStore ve OnBackPressedDispatcher cihazlarınıza bağlamanıza olanak tanır.

Örneğin, navigation scoped ViewModel kullanan bir parçayı test ederken TestNavHostController üzerinde setViewModelStore işlevini çağırmanız gerekir:

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())