choosing-what-to-test について
どのコードや状態をテストし、価値の低いテストを避けるかを判断する Android 開発者向けのスキルです。画面、メモリ、永続化、システム、エッジケース別に整理し、フレームワークの入口ではなく業務上の動作を重視します。
主な機能
- 01状態の種類ごとにテスト範囲を計画
- 02UI、ViewModel、データ層のテストを区別
- 03よくあるエラーとエッジケースを提示
- 04価値の低いフレームワーク入口のテストを特定
- 05テスト配置の判断材料を提供
活用シーン
- 01モジュールのテスト範囲を決定
- 02変更に対するテストの不足をレビュー
- 03Activity や Fragment の単体テスト要否を判断
- 04関数のエッジケーステストを追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill choosing-what-to-testインストール後、Claude Code で/choosing-what-to-testを入力して読み込まれたか確認します。
詳細
テスト対象の選び方 — 状態を網羅し、フレームワークのエントリーポイントは避ける
Android のテストスイートは、ビジネスロジックのテストが不足し、フレームワークとの接着部分のテストが過剰になりがちです。Google の /training/testing/fundamentals/what-to-test ページでは、コードがどの状態に触れるかに基づいてテストの価値を分類し、避けるべきテストのカテゴリを明示しています。このスキルでは、それらのカテゴリとエッジケースのチェックリストを定義しているため、「重要なものはすべてテストする」といった曖昧な回答ではなく、エージェントが具体的に助言できます。
このスキルを使う場面
- ユーザーが「このモジュールでは何のテストを書けばよいですか」や「X のテストは必要ですか」と尋ねたとき。
- ユーザーが
Activity、Fragment、Serviceをユニットテストするべきか尋ねたとき(回答:通常は不要です —「テストすべきでないもの」を参照してください)。 - ユーザーが PR をレビューしていて、テストの不足箇所が明確でないとき。
- ユーザーが「この関数で見落としているエッジケースは何ですか」と尋ねたとき。
- ユーザーが ViewModel、Repository、Composable のどこにテストの労力をかけるべきか尋ねたとき。
このスキルを使わない場面
- ユーザーがすでにテスト対象を把握していて、テスト範囲(小・中・大)を選んでいるとき —
../understanding-the-testing-pyramid/SKILL.mdを使ってください。 - ユーザーがフェイク/モック/スタブのどれを使うか選んでいるとき —
../../doubles/picking-test-doubles/SKILL.mdを使ってください。 - ユーザーがソースセット(
src/test/とsrc/androidTest/)を設定しているとき —../../strategies/organizing-test-source-sets/SKILL.mdを使ってください。
前提条件
- 少なくとも 1 つの
ViewModel、リポジトリ、または画面を含むモジュールがあること。 - エージェントが「データレイヤー」を実際のパスに対応づけられるよう、Android アーキテクチャのレイヤー(UI/Domain/Data)を理解していること。
../understanding-the-testing-pyramid/SKILL.mdの 3 層ピラミッドの考え方を把握していること。
テスト対象とする 5 つの状態カテゴリ
以下の 5 つの状態カテゴリは、ユーザーが求めた内容をまとめたものです。これらは developer.android.com/training/testing/fundamentals/what-to-test の記述そのもののセクションに対応しています(ページでは「必須のユニットテスト」「UI テスト」「エッジケースのテスト」といったレイヤー形式の見出しが使われていますが、根底にある軸はどの種類の状態が変化するかです)。
1. 画面上の状態 — ユーザーが今目にしているもの
描画されるもの:ボタンのラベル、リストの件数、有効/無効の状態、チェックボックスの切り替えなどです。UI テストで確認します。
"画面 UI テストでは、1 つの画面における重要なユーザー操作を確認します。ボタンのクリック、フォームへの入力、表示状態の確認などを行います。画面ごとに 1 つのテストクラスを用意するのが、よい出発点です。" —
developer.android.com/training/testing/fundamentals/what-to-test
"ユーザーフローテストまたはナビゲーションテストでは、最も一般的な経路の大半を確認します。これらのテストでは、ユーザーがナビゲーションフローを進む様子をシミュレートします。" —
developer.android.com/training/testing/fundamentals/what-to-test
Compose の場合は、onNodeWithTag(...).assertIsDisplayed()/assertTextEquals(...)/assertIsEnabled() でアサートします。Views の場合は、onView(...).check(matches(isDisplayed())) を使います。カテゴリ横断のスキル ../../../instrumentation/espresso/writing-espresso-tests/SKILL.md を使ってください。
2. メモリ上に保持される状態 — ViewModel とプレゼンターの状態
ViewModel.uiState、メモリ内キャッシュ、現在の選択状態などです。小規模なユニットテストで確認します。
"ViewModelまたはプレゼンターのユニットテスト。" —
developer.android.com/training/testing/fundamentals/what-to-test
ViewModel のテストでは、フェイクリポジトリを使って ViewModel を起動し、イベントを発生させて、公開されている StateFlow/LiveData をアサートします。../../../jvm-tests/coroutines/testing-coroutines-with-runtest/SKILL.md と ../../../jvm-tests/coroutines/testing-flows-with-turbine/SKILL.md を参照してください。
3. 永続化される状態 — DB、DataStore、SharedPreferences、ファイル
プロセス終了後も保持されるもの:
"データレイヤー、特にリポジトリのユニットテストです。データレイヤーの大部分はプラットフォームに依存しないようにしてください。そうすることで、テストではテスト用の代替実装を使って、データベースモジュールやリモートデータソースを置き換えられます。" —
developer.android.com/training/testing/fundamentals/what-to-test
2 種類あります。
- リポジトリのユニットテスト — ストレージソースをフェイクに置き換え、リポジトリのキャッシュ、マッピング、エラー処理を検証します。
- ストレージの統合テスト — Room/DataStore/SharedPreferences を実際の実装で動作させます。
/fundamentalsによれば、これらは小規模なインストルメンテーションテストです(「小規模なインストルメンテーションテスト:SQLite データベースなどのフレームワーク機能とコードが適切に連携することを検証できます」)。
4. その他の状態 — システムバー、システムサービス、アラーム、ブロードキャスト
ウィンドウインセットの状態、ダークモードのフラグ、アラームのスケジュール設定、バックグラウンドジョブの状態です。/what-to-test ページには独立した見出しはありませんが、/fundamentals ページではインストルメンテーションテストの項目で扱われています。シャドウが利用できる場合は Robolectric(中規模のローカルテスト)で、実際のシステムが必要な場合はインストルメンテーションテストで検証します。
5. エラーとエッジケース
"ユニットテストでは、通常のケースとエッジケースの両方に重点を置く必要があります。エッジケースとは、発生頻度が低く、人間によるテストや大規模なテストでは見落とされやすい状況です。たとえば、次のようなものがあります。
- 負の数、ゼロ、境界条件を使った数学演算。
- あらゆるネットワーク接続エラー。
- 不正な JSON などの破損データ。
- ファイル保存時にストレージがいっぱいになった状態のシミュレーション。
- プロセスの途中でオブジェクトが再作成される状況(デバイスの回転時のアクティビティなど)。" —
developer.android.com/training/testing/fundamentals/what-to-test
これは、どのようなテスト予算でも最も価値の高い項目です。人間は見落とし、大規模なテストでも見落としますが、フェイクを使った小規模なユニットテストなら 5 ms で検出できます。
テストしないもの(原文どおり)
このページは、この点について非常に明確です。
"価値が低いため、避けるべきユニットテストもあります。
- 自分のコードではなく、フレームワークやライブラリが正しく動作することを検証するテスト。
- アクティビティ、フラグメント、サービスなどのフレームワークのエントリポイントにはビジネスロジックを含めるべきではないため、ユニットテストを優先する必要はありません。アクティビティのユニットテストは、ほとんどがフレームワークコードを対象とし、セットアップも複雑になるため、価値がほとんどありません。UI テストなどのインストルメンテーションテストで、これらのクラスをテストできます。" —
developer.android.com/training/testing/fundamentals/what-to-test
実務上の影響:
- 絶対に行わないこと:
ActivityのonCreateでビューがインフレートされることを検証するRobolectricユニットテストを書くこと。これはフレームワークをテストしています。 - 絶対に行わないこと:
RoomDatabase.Builderのマイグレーション登録を検証するユニットテストを書くこと。これは Room をテストしています。 - 絶対に行わないこと:
@GETのパスが解決されることを検証するためにRetrofitをモックするユニットテストを書くこと。これは Retrofit をテストしています。 - 絶対に行わないこと: リファクタリングのたびに変わるプライベートな実装の詳細をテストすること。公開されている観測可能な動作をテストしてください。
- 推奨: あるクラスがユニットテストしにくい場合は、そのクラスからビジネスロジックを切り出してリファクタリングします(
Activity→ViewModel→UseCase→Repository)。ページは「テスト可能なアプリのアーキテクチャでは、コードが各部分を簡単に個別テストできる構造になっています」と述べて、この流れを締めくくっています(/fundamentalsから引用)。
このページでは、ほかのカテゴリにも簡単に触れています。
"スクリーンショットテスト、パフォーマンステスト、モンキーテストなど、より専門的なテストもあります。" —
developer.android.com/training/testing/fundamentals/what-to-test
これらは「何をテストするか」という判断の対象外ですが、ビジュアルのリグレッションやレンダリングの正確性について質問された場合は、言及する必要があります。関連カテゴリのスキル ../../../compose/audit/auditing-compose-test-suite/SKILL.md を参照してください。
パターン
パターン:間違い vs 正しい — ViewModel ではなく Activity をテストする
// WRONG
@RunWith(AndroidJUnit4::class)
class CheckoutActivityTest {
@get:Rule val rule = ActivityScenarioRule(CheckoutActivity::class.java)
@Test fun whenTotalIsNegative_showsError() {
rule.scenario.onActivity { activity ->
activity.viewModel.applyCoupon("FREE_BEER_NEGATIVE_PRICE")
// assert on the activity's TextView contents
assertEquals("Error", activity.findViewById<TextView>(R.id.error).text)
}
}
}
// WRONG because: the test rebuilds the framework lifecycle, the Activity, the inflater,
// and the View graph just to assert business logic that lives in the ViewModel. Per
// /what-to-test: "Unit tests for activities have little value, because they would cover
// mostly framework code".
// RIGHT — small, JVM, sub-50 ms
class CheckoutViewModelTest {
@Test fun applyCoupon_negativePrice_emitsError() = runTest {
val vm = CheckoutViewModel(FakePricingRepository(allowNegative = true))
vm.applyCoupon("FREE_BEER_NEGATIVE_PRICE")
assertEquals(CheckoutUiState.Error("Negative total"), vm.uiState.value)
}
}
Activity には、エラー文字列が描画されることを検証する薄いビッグテスト(Espresso / Compose-test)を引き続き用意します。ロジックは ViewModel のテストで検証します。2 つのレイヤーに、それぞれ 2 つの責務があります。
パターン:間違い vs 正しい — ユーティリティクラスを過剰にモックする
// WRONG
class FormatDurationTest {
private val clock = mockk<Clock>()
private val locale = mockk<Locale>()
@Test fun formatsMinutesAndSeconds() {
every { clock.now() } returns Instant.parse("2024-01-01T00:01:30Z")
every { locale.language } returns "en"
assertEquals("1m 30s", formatDuration(90.seconds, clock, locale))
}
}
// WRONG because: this is a pure-function utility per /what-to-test ("Unit tests for
// utility classes such as string manipulation and math"). Mocks add ceremony with
// no value. Pass real values.
// RIGHT
class FormatDurationTest {
@Test fun formatsMinutesAndSeconds() {
assertEquals("1m 30s", formatDuration(90.seconds))
}
@Test fun handlesZero() {
assertEquals("0s", formatDuration(0.seconds))
}
@Test fun handlesHoursBoundary() {
assertEquals("1h 0m 0s", formatDuration(3600.seconds))
}
}
パターン:間違い vs 正しい — エッジケースのチェックリストを省略する
// WRONG — only the happy path
class ParseAmountTest {
@Test fun parsesValidAmount() = assertEquals(12.34, parseAmount("12.34"))
}
// RIGHT — edge cases per /what-to-test
class ParseAmountTest {
@Test fun parsesValidAmount() = assertEquals(12.34, parseAmount("12.34"))
@Test fun parsesZero() = assertEquals(0.0, parseAmount("0"))
@Test fun parsesNegative() = assertEquals(-5.0, parseAmount("-5"))
@Test fun parsesScientific() = assertEquals(1e6, parseAmount("1e6"))
@Test fun rejectsEmpty() = assertNull(parseAmount(""))
@Test fun rejectsMalformed() = assertNull(parseAmount("12.34.56"))
@Test fun rejectsLeadingWhitespace() = assertNull(parseAmount(" 12"))
@Test fun rejectsLocaleComma() = assertNull(parseAmount("12,34")) // German format
@Test fun handlesMaxDouble() = assertEquals(Double.MAX_VALUE, parseAmount("1.7976931348623157E308"))
}
エッジケース洗い出しチェックリスト
ユニットテストに合格したと判断する前に、このチェックリストを確認してください。各項目は /training/testing/fundamentals/what-to-test の原文リストと、Compose/コルーチンのコード向けに skydoves が追加した項目に対応しています。
- 数値の境界値 — ゼロ、負の値、
Int.MAX_VALUE、Long.MIN_VALUE、Double.NaN、Double.POSITIVE_INFINITY。 - 空/単一/複数のコレクション —
emptyList()、listOf(x)、数千項目を含むリスト。 - Null — nullable な入力をすべて確認します。型システム上は許容されない場合でも、デシリアライズによって null が生成されることがあります(
coerceInputValuesを使用した Moshi/kotlinx-serialization)。 - 不正な文字列 — 空、空白のみ、長さ 1、長さ MAX、非 ASCII、RTL テキスト、絵文字、ZWJ シーケンス、サロゲートペア。
- ロケールの違い — ドイツ語の数値形式
12,34、アラビア数字٠١٢、トルコ語のドット付き i。 - ネットワークエラー —
IOException、HTTP 4xx、HTTP 5xx、不正な JSON、途中で切れたストリーム、タイムアウト。 - ストレージエラー — ディスク容量不足、権限拒否、破損した SQLite、暗号化された設定。
- プロセス終了/状態の復元 — このページにある「プロセスの途中でオブジェクトが再作成される場合(デバイスの回転時の Activity など)」を確認します。
rememberSaveable、SavedStateHandle、Bundle からの復元をカバーします。 - 並行処理 — 2 つのコルーチンから同じ操作を行う、中断中のキャンセル、遅いプロデューサーと速いコンシューマー(
kotlinx-coroutines-testを使用)。 - 時間 — DST の切り替わり、うるう秒、日付をまたぐ時刻、タイムゾーンの変更、単調時計と壁時計。
- 構成の変更 — ダークモードの切り替え、ロケールの変更、フォントスケール 1.5x および 2.0x、RTL レイアウト。
前半は Google の原文リストそのままです。後半は、最新の Android 向けに実用面を拡張した項目です。
判断表 — このコードにテストを用意すべきか?
What does the code touch? Test? Where?
-----------------------------------------------------------------------------
Pure function / utility (string, math, mapping) YES src/test/ (small)
ViewModel / presenter state YES src/test/ (small)
Repository / use case (fake the storage) YES src/test/ (small)
Real Room / DataStore YES src/androidTest/ (small instrumented)
Composable rendering of state YES src/androidTest/ or src/test/ (Robolectric)
Navigation flow (multi-screen) YES src/androidTest/ (big)
Activity onCreate / inflation logic NO framework code
Fragment onViewCreated wiring NO framework code
Service onStartCommand wiring NO framework code
Retrofit interface (annotation routing) NO library code
Hilt module declarations NO library code
Compose Modifier internals NO library code
Generated code NO generated
Private function with no public observable effect NO test the public caller
必須ルール
- 必須: 公開された観測可能な動作(
ViewModelが公開する状態、Repositoryの戻り値、描画されたセマンティクス)をテストします。非公開の内部実装はテストしません。 - 禁止: ビジネスロジックについて、フレームワークのエントリーポイント(
Activity、Fragment、Service)をユニットテストすること。代わりにロジックを切り出してください。/what-to-testによれば、「フレームワークのエントリーポイント……にはビジネスロジックを持たせるべきではないため、ユニットテストを優先する必要はありません」。 - 必須: マージ前に、原文リストにあるエッジケース(数値の境界値、ネットワークエラー、不正なデータ、ストレージ容量不足、プロセスの再作成)をカバーします。
- 禁止: フレームワークやサードパーティライブラリの動作を検証すること。「コードではなく、フレームワークまたはライブラリが正しく動作するかを検証するテスト」は、明示的な回避対象です。
- 推奨: 画面上の状態をカバーするため、画面ごとに Compose/Espresso のビッグテストを 1 つ用意します。それ以外は小規模な ViewModel テストを活用します。
- 推奨: クラスのテストが難しい場合は、それをアーキテクチャ上の兆候として捉えます。プロダクションコードが多くのことをしすぎているか、誤ったレイヤーに状態を保持している可能性があります。
検証
- モジュール内のすべての
ViewModelとRepositoryの公開関数に、少なくとも1つの小規模なテストがあります。 - すべての画面に、
assertIsDisplayed/check(matches(isDisplayed()))を使って「画面上の状態」を検証する、大規模または中規模のテストが少なくとも1つあります。 - フレームワークが処理する動作を検証するためだけに、
Activity/Fragment/Serviceのみを対象にするテストクラスはありません。 - 自明でない入力範囲を持つ新しい公開関数について、上記のエッジケースチェックリストを確認します。
-
Retrofit、Room、Hilt、またはComposeの内部動作を検証するために、それらをモックするテストはありません。 - PR テンプレート(またはレビュー用チェックリスト)では、マージ前に「このコードが触れる状態は何か」への回答を求めます。
参考資料
developer.android.com/training/testing/fundamentals/what-to-test— このスキル内のすべての引用の正式な出典です。developer.android.com/training/testing/fundamentals— テスト可能なアーキテクチャの根拠と、「すべてのユニットテストがローカルで実行されるわけではありません」という引用。developer.android.com/training/testing/local-tests— 「注意:複雑なモックは避けてください。代わりに、フェイクなど、さまざまな種類のテストダブルを使用できます。」developer.android.com/training/testing/instrumented-tests— 画面上の状態を検証するテストで、インストルメンテーションと Robolectric のどちらを使うべきかについて説明しています。tasks/research/R8-android-fundamentals.md— 「Essential unit tests」/「Testing Edge Cases」/「Unit Tests to Avoid」からの逐語引用。- 関連スキル:
../understanding-the-testing-pyramid/SKILL.md、../../doubles/picking-test-doubles/SKILL.md、../../strategies/applying-testing-strategies/SKILL.md、../../strategies/organizing-test-source-sets/SKILL.md。 - カテゴリー横断の参考資料:
../../../jvm-tests/coroutines/testing-coroutines-with-runtest/SKILL.md、../../../jvm-tests/coroutines/testing-flows-with-turbine/SKILL.md、../../../jvm-tests/mocking/mocking-with-mockito/SKILL.md、../../../jvm-tests/mocking/mocking-with-mockk/SKILL.md、../../../instrumentation/espresso/writing-espresso-tests/SKILL.md、../../../instrumentation/scenarios/launching-activities-with-activityscenario/SKILL.md、../../../instrumentation/scenarios/launching-fragments-with-fragmentscenario/SKILL.md、../../../compose/audit/auditing-compose-test-suite/SKILL.md。