アプリを公開する前にナビゲーション ロジックをテストして、アプリが想定どおりに機能することを検証してください。
Navigation コンポーネントは、デスティネーション間のナビゲーションの管理、引数の受け渡し、FragmentManager の操作に関するすべての作業を処理します。このような機能はすでに厳密にテストされているため、個々のアプリで再度テストする必要はありません。他方、フラグメント内のアプリ固有のコードとその NavController との間のインタラクションに関しては、デベロッパーがテストを行う必要があります。
単独でのテスト
フラグメントのインタラクションを NavController とは独立にテストするため、Navigation 2.3 以上では、現在のデスティネーションを設定するための API を提供する TestNavHostController が用意されており、NavController.navigate() オペレーションの後でバックスタックを検証できます。
プロジェクトにナビゲーション テストのアーティファクトを追加するには、アプリ モジュールの build.gradle ファイルに次の依存関係を追加します。
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") }
トリビアゲームを考えてみましょう。ゲームは title_screen で始まり、ユーザーがプレイをタップすると 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 画面に適切に移動するかどうかをテストするには、このフラグメントが NavController を正しく R.id.in_game 画面に移動させるかどうかを検証する必要があります。
FragmentScenario、Espresso、TestNavHostController を組み合わせて使用すると、このシナリオのテストに必要な条件を再現できます。次の例をご覧ください。
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);
}
}
上記の例では、TestNavHostController のインスタンスを作成し、それをフラグメントに割り当てています。次に、Espresso を使用して UI を動作させ、適切なナビゲーション アクションが実行されるかどうかを検証しています。
実際の NavController と同じように、setGraph を呼び出して TestNavHostController を初期化する必要があります。この例では、テスト対象のフラグメントはグラフの開始デスティネーションでした。TestNavHostController に用意されている setCurrentDestination メソッドを使用すると、現在のデスティネーション(およびオプションでそのデスティネーションの引数)を設定して、テスト開始前に NavController を正しい状態に置くことができます。
NavHostFragment が使用する NavHostController インスタンスとは異なり、navigate() を呼び出したとき、TestNavHostController はその 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、ViewModelStore、OnBackPressedDispatcher に接続できるようにします。
たとえば、ナビゲーションにスコープ設定された 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())
関連トピック
- インストゥルメント化単体テストを作成する - インストゥルメント化テストスイートをセットアップして、Android 搭載デバイス上でテストを実行する方法について学びます。
- Espresso - Espresso を使用してアプリの UI をテストします。
- AndroidX Test の JUnit4 ルール - AndroidX Test ライブラリの JUnit4 ルールを使用することで、柔軟性を高め、テストで必要となるボイラープレート コードを削減します。
- アプリのフラグメントをテストする -
FragmentScenarioを使用してアプリ フラグメントを単独でテストする方法について学びます。 - AndroidX Test 向けにプロジェクトをセットアップする - AndroidX Test を使用するためにアプリのプロジェクト ファイル内で必要なライブラリを宣言する方法について学びます。