android-testing について
テストファースト、Composeテスト、Kotlin Multiplatformの注意点を扱うAndroidテスト向けリファレンススキルです。コルーチンスケジューラー、ネイティブ向けテストのコンパイル、スクリーンショットテストに関する問題を避け、適切なテスト形式を選ぶための情報をまとめています。
主な機能
- 01RED-GREEN-REFACTORのテスト手順
- 02KMPのテスト名と対象確認
- 03Composeテストのスケジューラー処理
- 04UI選択でセマンティクスを優先
- 05UI・統合・スクリーンショットテストの選択
- 06決定論的なスクリーンショット準備
活用シーン
- 01本番コード作成前の動作検証
- 02iOS対象のcommonTestコンパイルエラー調査
- 03Composeのコルーチンとアニメーションのテスト
- 04画面状態の安定したスクリーンショットテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rcosteira79/ android-skills --skill android-testingインストール後、Claude Code で/android-testingを入力して読み込まれたか確認します。
詳細
Android のテスト
テストファーストを基盤とする Android 固有のテストです。このリファレンスでは、3 つのテスト層の基本、モックよりフェイクを使うこと、runTest、Given-When-Then の命名といった基礎ではなく(ただし、名前によっていずれかのターゲットでコンパイルエラーになる場合を除きます)、テストファーストの規律と、間違いやすい Compose テストおよび KMP の落とし穴に焦点を当てます。
テストファースト(基盤)
- まず失敗するテストを書かずに、本番コードを書いてはいけません。 テストを書き、失敗することを確認して(RED)、テストを通す最小限のコードを書き(GREEN)、その後にリファクタリングします。失敗することを一度も確認していない振る舞いは、証明されていません。
- バグが原因で失敗していたテストが成功するようになるまで、バグを修正したとは言えません。
これは、専用の TDD 規律スキル(superpowers:test-driven-development、ace:test-driven-development)に加えて適用されますが、いずれも必須ではありません。テストスタックをゼロから立ち上げる(テスト DI、JUnit/Robolectric/Roborazzi/Paparazzi の選定、計装ランナー、Compose Preview Screenshot Testing、UI Automator、Jacoco)方法については、Google 公式の testing-setup スキル(android skills add testing-setup)を参照してください。
commonTest の名前は JVM だけでなく Kotlin/Native でも通る必要があります
commonTest は宣言されたターゲットごとに一度ずつコンパイルされるため、jvmTest が成功しても iOS のコンパイルについては何も証明されません。バッククォート自体はどのターゲットでも問題ありません。名前の一部ではなく、エスケープ構文だからです。ただし、Kotlin/Native のフロントエンドはバッククォート内の 24 文字を拒否します。. ; , ( ) [ ] { } / < > : \ $ & ~ * ? # | § % @(FirNativeIdentifierChecker — Kotlin 1.7 以降、無条件のエラーです。この文字セットでは、IR マングラー独自の文字も予約されています)。Kotlin/JVM で禁止されているのは . ; [ ] / < > : \ だけです。**2 つのリストの違いはカンマで、スペースはいずれにも含まれません。**そのため、カンマを使う Given-When-Then の命名規則は、CI が成功したまま、すべてのネイティブターゲットで失敗します。
// commonTest — compiles for jvm; fails compileTestKotlinIosSimulatorArm64 with
// error: name contains illegal characters: ","
@Test fun `given an empty cart, when an item is added, then the total updates`() { … }
// commonTest — same sentence, commas dropped. Backticks and spaces compile everywhere.
@Test fun `given an empty cart when an item is added then the total updates`() { … }
ここから 2 つのルールが導かれます。すべての commonTest 宣言名で , . ( ) : ? を使わないでください。このチェックの対象は fun だけでなく、クラス、プロパティ、型エイリアス、値パラメーター、列挙型のエントリーです。また、KMP テストは jvmTest ではなく、allTests または <target>Test で検証してください。jvmTest が依存するのは compileTestKotlinJvm だけです。デバイス専用ターゲットにはテストタスクがないため、テストバイナリを明示的にコンパイルしてください(linkDebugTestIosArm64)。
これを、Kotlin のコーディング規約で言及される Android のルールと混同しないでください。D8 は minSdk 30 未満でスペースを拒否しますが、影響を受けるのは計装テストだけです。また、API レベルを問わず、DEX の SimpleName 文法で認められていないカンマが問題になることはありません。
Compose テストのデフォルトは StandardTestDispatcher になりました
createComposeRule() / runComposeUiTest {} のデフォルトは StandardTestDispatcher です(kotlinx.coroutines.test.runTest と同じです)。**別の「v2」パッケージはありません。**これは androidx.compose.ui.test.ComposeUiTestFlags.isStandardTestDispatcherSupportEnabled で制御されます(デフォルトは true)。そのため、通常の androidx.compose.ui.test.junit4.createComposeRule でもすでにこれが使われています。スケジューラーを固定するには、createComposeRule(effectContext = StandardTestDispatcher()) のように渡してください。このデフォルトでは、以前はすぐに実行されていた LaunchedEffect(旧 UnconfinedTestDispatcher の動作)も、キューに入った処理を消化するために mainClock.advanceTimeBy(0) / runCurrent() を明示的に呼び出す必要がある場合があります。従来の動作に一時的に戻す場合に限り、フラグを false に設定してください。
2 つのスケジューラーの落とし穴(コルーチンテストで注意すべき唯一の問題):MainDispatcherRule の TestDispatcher と、runTest { } が作成するディスパッチャーは、それぞれ別の TestCoroutineScheduler を使います。Dispatchers.Main とテスト本体で 1 つを共有するには、runTest(mainRule.dispatcher) に mainRule.dispatcher を渡してください。そうしないと、advanceUntilIdle() が片方しか処理せず、アサーションと ViewModel の間で競合が発生します。
まずセマンティクスを優先し、testTag はフォールバックとして使う
testTag より、ユーザーに見えるセマンティクスを優先してください。実際のユーザーやスクリーンリーダーにはセマンティクスが見えますが、testTag はテスト以外には見えません。セレクターの優先順位は、(1)onNodeWithText、(2)onNodeWithContentDescription、(3)ロール/状態のマッチャー(hasClickAction()、isSelected()、isFocused()、isEnabled())、(4)安定したユーザー可視テキストがない場合、またはテキストが重複して曖昧な場合(同一行のリスト、ロケールごとの文言、複数のインスタンス)のみonNodeWithTagです。テキストのアサーションはリファクタリング後も機能し、アクセシビリティもテストできます。一方、testTagのアサーションはタグが変わった瞬間に壊れ、ユーザーに見えるリグレッションを見逃します。
(知っておく価値のある異論:skydoves/android-testing-skillsは、i18nへの堅牢性を理由にタグを優先することを主張しており、androidx/material3自体におけるtestTag:onNodeWithText:onNodeWithContentDescriptionの比率が1825:424:46であることを根拠としています。アクセシビリティを別途カバーしているなら妥当な考え方です。このスキルでは、testTagでは決して検出できない種類のバグを見つけられるため、セマンティクス優先をデフォルトとしています。)
テスト対象としてコールバックを使う
コンポーザブルの契約は「状態を描画し、コールバックを発行する」です。まさにそれをテストしてください。アサーションをViewModelのモック経由で行わないでください。
@Test fun `tapping article row invokes onArticleClick with id`() {
var clickedId: String? = null
composeTestRule.setContent { ArticleRow(Article(id = "42", title = "Hello"), onArticleClick = { clickedId = it }) }
composeTestRule.onNodeWithText("Hello").performClick()
assertEquals("42", clickedId)
}
テストの形式を選ぶ — 振る舞いを証明できる最小限の契約
| 証明すること | 形式 |
|---|---|
| 描画されたテキスト、条件付きコンテンツ、読み込み/エラーの分岐、コールバックの接続 | 通常のCompose UIテスト(状態+コールバック、ナビゲーショングラフなし) |
| フォーカス移動、キーボード、TV/Dパッド | performKeyInput+assertIsFocused()を使ったComposeテスト(compose/references/focus-navigation.mdを参照) |
| セマンティクスでは証明できないビジュアル上の契約 — 間隔、テーマカラー、タイポグラフィ、エレベーション、グラデーション、スケルトン | スクリーンショットテスト。意味のある状態ごとに1つ |
| State holderがUIを正しく更新すること | State holderのユニットテスト+配線を確認するスモークテストを1つ |
| ライフサイクル、ナビゲーション、またはDI連携そのもの | 統合テスト(createAndroidComposeRule、Hiltルール、実際のグラフ) |
**テストクロックと実時間のクロック:**Composeの状態から観測できる条件には、mainClock.advanceTimeUntil(ms) { state.value == Done }を使ってください(決定論的で高速です)。rule.waitUntil(ms) { … }は、Compose以外の条件(Job.isCompleted、外部カウンター)にのみ使ってください。1つのテスト内で両方を混在させると、よくフレークの原因になります。
**アニメーションテストでは、setContentより前にmainClock.autoAdvance = falseを設定する必要があります。**設定しない場合、不定時間のアニメーションではフレームワークのInfiniteAnimationPolicyがCancellationExceptionをスローし、有限時間のアニメーションは自動進行によって一気に終了するため、中間状態を観測できません。一時停止した後、まずadvanceTimeByFrame()(開始)を実行し、続けてadvanceTimeBy(durationMillis)でフレームを進めてください。
**スクリーンショットの決定性:**状態データは固定してください(現在時刻、乱数シード、スクリーンショットの経路内のリモートURLを使わない)。クロック(Clock.fixed(...))とアニメーションの進行状況を固定し、画像の多い画面には偽の画像ローダーを使ってください(Coil 3のテスト用代替実装:LocalImageLoader provides FakeImageLoader(LocalContext.current)、またはsetContentWithFakeImageLoader { … })。UI要素ごとではなく、意味のある状態(読み込み/エラー/成功/空)ごとに1枚撮影してください。