Test amaçlı gezinme

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.

Gezinme bileşeni; hedefler arasında gezinmeyi yönetme, bağımsız değişkenler geçirme ve FragmentManager ile çalışma işlemlerinin tamamını gerçekleştirir. Bu özellikler zaten titizlikle test edildiğinden uygulamanızda tekrar test etmenize gerek yoktur. Ancak test etmeniz önemli olan, parçalarınızdaki uygulamaya özel kod ile bunların NavController arasındaki etkileşimlerdir. Bu kılavuzda, sık karşılaşılan birkaç gezinme senaryosu ve bunların nasıl test edileceği adım adım açıklanmaktadır.

Parçada gezinmeyi test et

Gezinme 2.3 ve sonraki sürümler, NavController ile parça etkileşimlerini ayrı ayrı test etmek amacıyla, geçerli hedefi ayarlamak ve NavController.navigate() işlemlerinden sonra arka yığını doğrulamak için API'ler sağlayan bir TestNavHostController sunar.

Uygulama modülünüzün build.gradle dosyasına aşağıdaki bağımlılığı ekleyerek Gezinme Testi yapısını projenize ekleyebilirsiniz:

Modern

dependencies {
  def nav_version = "2.7.7"

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

Kotlin

dependencies {
  val nav_version = "2.7.7"

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

Bir bilgi yarışması oluşturduğunuzu varsayalım. Oyun bir title_screen ile başlar ve kullanıcı oynat düğmesini tıkladığında in_game ekranına gider.

title_screen temsil eden parça aşağıdaki gibi 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ı Oynat'ı tıkladığında uygulamanın kullanıcıyı in_game ekranına doğru şekilde yönlendirip yönlendirmediğini test etmek amacıyla testinizin, bu parçanın NavController öğesini R.id.in_game ekranına doğru şekilde taşıdığını doğrulaması gerekir.

FragmentScenario, Espresso ve TestNavHostController kombinasyonunu kullanarak bu senaryoyu test etmek için gerekli koşulları aşağıdaki örnekte gösterildiği gibi 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 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);
    }
}

Yukarıdaki örnek, TestNavHostController öğesinin bir örneğini oluşturur ve bunu parçaya atar. 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 işleminde olduğu gibi, TestNavHostController öğesini başlatmak için setGraph yöntemini çağırmanız gerekir. Bu örnekte test edilen parça, grafiğimizin başlangıç hedefiydi. TestNavHostController, testiniz başlamadan önce NavController öğesinin doğru durumda olması için geçerli hedefi (ve isteğe bağlı olarak söz konusu hedefe ilişkin bağımsız değişkenleri) ayarlamanıza olanak tanıyan bir setCurrentDestination yöntemi sunar.

NavHostFragment tarafından kullanılacak NavHostController örneğinden farklı olarak TestNavHostController, navigate() çağırdığınızda temel navigate() davranışını (FragmentNavigator'ın yaptığı FragmentTransaction gibi) tetiklemez ve yalnızca TestNavHostController durumunu günceller.

NavigationUI'yi Fragment senaryosuyla test etme

Önceki örnekte, titleScenario.onFragment() için sağlanan geri çağırma, parça yaşam döngüsü boyunca RESUMED durumuna taşındıktan sonra çağrılır. Bu zamana kadar parçanın görünümü önceden oluşturulmuş ve eklenmiştir. Dolayısıyla, yaşam döngüsünde düzgün bir şekilde test edilemeyecek kadar geç olabilir. Örneğin, parçanızdaki görünümlerle NavigationUI kullanırken (örneğin, 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, TestNavHostController cihazınızı yaşam döngüsünün erken aşamalarında ayarlamak için bir yönteme ihtiyacınız vardır.

Kendi Toolbar değerine 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 oluşturulan NavController değerine ihtiyacımız vardır. Önceki onFragment() yaklaşımını kullanmak, TestNavHostController çözümümüzün yaşam döngüsünde çok geç olmasına neden olarak findNavController() çağrısının başarısız olmasına neden olur.

FragmentScenario, yaşam döngüsü olayları için geri çağırmaları kaydetmek üzere kullanılabilecek bir FragmentFactory arayüzü sunar. Bu, aşağıdaki örnekte gösterildiği gibi onCreateView() ile hemen sonra gelen bir geri çağırma almak için Fragment.getViewLifecycleOwnerLiveData() ile birleştirilebilir:

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. Bu da parçanın kilitlenmeden NavigationUI yöntemlerini kullanmasına olanak tanır.

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

TestNavHostController, arka yığın girişleriyle etkileşim kurarken denetleyiciyi NavHostController'dan devraldığı API'leri kullanarak kendi test LifecycleOwner, ViewModelStore ve OnBackPressedDispatcher testinize bağlamanıza olanak tanır.

Örneğin, gezinme kapsamlı ViewModel kullanan bir parçayı test ederken TestNavHostController üzerinde setViewModelStore çağrısı yapmanı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())