アプリを公開する前にナビゲーション ロジックをテストして、アプリが想定どおりに機能することを検証してください。
Navigation コンポーネントは、デスティネーション間のナビゲーションの管理、引数の受け渡し、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 で始まり、ユーザーがプレイをタップすると 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
インスタンスとは異なり、navigate()
を呼び出したとき、TestNavHostController
はその 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 Test の JUnit4 ルール - AndroidX Test ライブラリの JUnit4 ルールを使用することで、柔軟性を高め、テストで必要となるボイラープレート コードを削減します。
- アプリのフラグメントをテストする -
FragmentScenario
を使用してアプリ フラグメントを単独でテストする方法について学びます。 - AndroidX Test 向けにプロジェクトをセットアップする - AndroidX Test を使用するためにアプリのプロジェクト ファイル内で必要なライブラリを宣言する方法について学びます。