애플리케이션이 예상대로 작동하는지 확인하기 위해 출시 전에 앱의 탐색 로직을 테스트해야 합니다.
탐색 구성요소는 대상 간의 탐색 관리, 인수 전달 및 FragmentManager
사용과 관련된 모든 작업을 처리합니다.
이러한 기능은 이미 엄격하게 테스트되었으므로 앱에서 다시 테스트할 필요는 없습니다. 그러나 테스트에 중요한 사항은 프래그먼트의 앱 관련 코드와 NavController
간의 상호작용입니다.
이 가이드에서는 몇 가지 일반적인 탐색 시나리오와 시나리오를 테스트하는 방법에 관해 설명합니다.
프래그먼트 탐색 테스트
NavController
와의 프래그먼트 상호작용을 개별적으로 테스트할 수 있도록 Navigation 2.3 이상에서는 현재 대상을 설정하기 위한 API를 제공하는 TestNavHostController
가 제공되며 NavController.navigate()
작업 이후 백 스택이 확인됩니다.
앱 모듈의 build.gradle
파일에 다음 종속 항목을 추가하여 프로젝트에 탐색 테스트 아티팩트를 추가할 수 있습니다.
dependencies { def nav_version = "2.8.6" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
dependencies { val nav_version = "2.8.6" androidTestImplementation("androidx.navigation:navigation-testing:$nav_version") }
예를 들어 상식 퀴즈 게임을 빌드하고 있다고 가정해 보겠습니다. 게임은 title_screen으로 시작하고 사용자가 Play를 클릭하면 in_game 화면으로 이동합니다.
title_screen을 나타내는 프래그먼트는 다음과 같을 수 있습니다.
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)
}
}
}
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 화면으로 적절하게 이동하는지 테스트하려면 테스트에서 이 프래그먼트가 NavController
를 R.id.in_game
화면으로 올바르게 이동하는지 확인해야 합니다.
FragmentScenario
, Espresso 및 TestNavHostController
의 조합을 사용하면 다음 예와 같이 이 시나리오를 테스트하는 데 필요한 조건을 재현할 수 있습니다.
@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)
}
}
@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
를 초기화해야 합니다. 이 예에서는 테스트 중인 프래그먼트가 그래프의 시작 대상이었습니다. TestNavHostController
는 setCurrentDestination
메서드를 제공합니다. 이 메서드를 통해 테스트가 시작되기 전에 NavController
가 올바른 상태에 있도록 현재 대상(및 선택적으로 현재 대상의 인수)을 설정할 수 있습니다.
NavHostFragment
가 사용하는 NavHostController
인스턴스와 달리 TestNavHostController
는 navigate()
를 호출할 때 기본 navigate()
동작(예: FragmentNavigator
가 실행하는 FragmentTransaction
)을 트리거하지 않으며 TestNavHostController
의 상태만 업데이트합니다.
FragmentScenario로 NavigationUI 테스트
앞의 예에서 titleScenario.onFragment()
에 제공된 콜백은 프래그먼트가 수명 주기를 통해 RESUMED
상태로 전환한 이후에 호출됩니다. 이때 프래그먼트의 뷰는 이미 생성되고 연결되었으므로 수명 주기에서 너무 늦어 적절한 테스트가 어려울 수 있습니다. 예를 들어 프래그먼트에서 제어하는 Toolbar
와 같이 프래그먼트의 뷰와 함께 NavigationUI
를 사용할 때 프래그먼트가 RESUMED
상태에 도달하기 전에 NavController
를 사용하여 설정 메서드를 호출할 수 있습니다. 따라서 수명 주기 초기에 TestNavHostController
를 설정하는 방법이 필요합니다.
자체 Toolbar
를 갖는 프래그먼트는 다음과 같이 작성하면 됩니다.
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)
}
}
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()
바로 뒤에 오는 콜백을 수신할 수 있습니다.
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)
}
}
}
}
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
, ViewModelStore
및 OnBackPressedDispatcher
에 연결할 수 있습니다.
예를 들어 탐색 범위 지정 ViewModel을 사용하는 프래그먼트를 테스트할 때 다음과 같이 TestNavHostController
에서 setViewModelStore
를 호출해야 합니다.
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())
TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());
// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(new ViewModelStore())
관련 주제
- 계측 단위 테스트 빌드 - 계측 테스트 도구 모음의 설정 방법과 Android 기기에서 테스트를 실행하는 방법을 알아봅니다.
- Espresso - Espresso로 앱 UI를 테스트합니다.
- AndroidX 테스트에 JUnit4 규칙 사용 - AndroidX 테스트 라이브러리에서 JUnit 4 규칙을 사용하여 유연성을 높이고 테스트에 필요한 상용구 코드를 줄입니다.
- 앱 프래그먼트 테스트 -
FragmentScenario
를 사용하여 앱 프래그먼트를 개별적으로 테스트하는 방법을 알아봅니다. - AndroidX 테스트용 프로젝트 설정 - AndroidX 테스트를 사용하기 위해 앱 프로젝트 파일에 필요한 라이브러리를 선언하는 방법을 알아봅니다.