탐색 테스트

애플리케이션이 예상대로 작동하는지 확인하기 위해 출시 전에 앱의 탐색 로직을 테스트해야 합니다.

탐색 구성요소는 대상 간의 탐색 관리, 인수 전달 및 FragmentManager 사용과 관련된 모든 작업을 처리합니다. 이러한 기능은 이미 엄격하게 테스트되었으므로 앱에서 다시 테스트할 필요는 없습니다. 그러나 테스트에 중요한 사항은 프래그먼트의 앱 관련 코드와 NavController 간의 상호작용입니다. 이 가이드에서는 몇 가지 일반적인 탐색 시나리오와 시나리오를 테스트하는 방법에 관해 설명합니다.

프래그먼트 탐색 테스트

NavController와의 프래그먼트 상호작용을 개별적으로 테스트할 수 있도록 Navigation 2.3 이상에서는 현재 대상을 설정하기 위한 API를 제공하는 TestNavHostController가 제공되며 NavController.navigate() 작업 이후 백 스택이 확인됩니다.

앱 모듈의 build.gradle 파일에 다음 종속 항목을 추가하여 프로젝트에 탐색 테스트 아티팩트를 추가할 수 있습니다.

Groovy

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

예를 들어 상식 퀴즈 게임을 빌드하고 있다고 가정해 보겠습니다. 게임은 title_screen으로 시작하고 사용자가 Play를 클릭하면 in_game 화면으로 이동합니다.

title_screen을 나타내는 프래그먼트는 다음과 같을 수 있습니다.

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

사용자가 Play를 클릭할 때 앱에서 사용자가 in_game 화면으로 적절하게 이동하는지 테스트하려면 테스트에서 이 프래그먼트가 NavControllerR.id.in_game 화면으로 올바르게 이동하는지 확인해야 합니다.

FragmentScenario, EspressoTestNavHostController의 조합을 사용하면 다음 예와 같이 이 시나리오를 테스트하는 데 필요한 조건을 재현할 수 있습니다.

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

위의 예에서는 TestNavHostController의 인스턴스를 생성하여 프래그먼트에 할당합니다. 그런 다음, Espresso를 사용하여 UI를 만들고 적절한 탐색 작업이 실행되는지 확인합니다.

실제 NavController와 마찬가지로 setGraph를 호출하여 TestNavHostController를 초기화해야 합니다. 이 예에서는 테스트 중인 프래그먼트가 그래프의 시작 대상이었습니다. TestNavHostControllersetCurrentDestination 메서드를 제공합니다. 이 메서드를 통해 테스트가 시작되기 전에 NavController가 올바른 상태에 있도록 현재 대상(및 선택적으로 현재 대상의 인수)을 설정할 수 있습니다.

NavHostFragment가 사용하는 NavHostController 인스턴스와 달리 TestNavHostControllernavigate()를 호출할 때 기본 navigate() 동작(예: FragmentNavigator가 실행하는 FragmentTransaction)을 트리거하지 않으며 TestNavHostController의 상태만 업데이트합니다.

FragmentScenario로 NavigationUI 테스트

앞의 예에서 titleScenario.onFragment()에 제공된 콜백은 프래그먼트가 수명 주기를 통해 RESUMED 상태로 전환한 이후에 호출됩니다. 이때 프래그먼트의 뷰는 이미 생성되고 연결되었으므로 수명 주기에서 너무 늦어 적절한 테스트가 어려울 수 있습니다. 예를 들어 프래그먼트에서 제어하는 Toolbar와 같이 프래그먼트의 뷰와 함께 NavigationUI를 사용할 때 프래그먼트가 RESUMED 상태에 도달하기 전에 NavController를 사용하여 설정 메서드를 호출할 수 있습니다. 따라서 수명 주기 초기에 TestNavHostController를 설정하는 방법이 필요합니다.

자체 Toolbar를 갖는 프래그먼트는 다음과 같이 작성하면 됩니다.

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

여기에서 onViewCreated()가 호출될 때 미리 생성된 NavController가 필요합니다. onFragment()의 이전 접근 방식을 사용하면 수명 주기에서 TestNavHostController가 너무 늦게 설정되어 findNavController() 호출이 실패합니다.

FragmentScenario는 수명 주기 이벤트에 콜백을 등록하는 데 사용할 수 있는 FragmentFactory 인터페이스를 제공합니다. 아래 예와 같이 이 클래스는 Fragment.getViewLifecycleOwnerLiveData()와 결합하여 onCreateView() 바로 뒤에 오는 콜백을 수신할 수 있습니다.

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

이 기법을 사용하면 onViewCreated()가 호출되기 전에 NavController를 사용할 수 있으므로 프래그먼트가 비정상 종료되지 않고 NavigationUI 메서드를 사용할 수 있습니다.

백 스택 항목과의 상호작용 테스트

백 스택 항목과 상호작용할 때 TestNavHostController를 사용하면 NavHostController에서 상속한 API를 사용하여 컨트롤러를 자체 테스트 LifecycleOwner, ViewModelStoreOnBackPressedDispatcher에 연결할 수 있습니다.

예를 들어 탐색 범위 지정 ViewModel을 사용하는 프래그먼트를 테스트할 때 다음과 같이 TestNavHostController에서 setViewModelStore를 호출해야 합니다.

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