compose-ui-testing-patterns について
Compose UI のテストを、動作契約を確認できる最小の範囲で設計するスキルです。制御された状態とセマンティクスのアサーションを重視し、必要に応じて統合テストやスクリーンショットテストを選びます。
主な機能
- 01最小のテスト境界の選択
- 02状態駆動の Compose UI テスト
- 03セマンティクスとコールバックの検証
- 04決定的なテスト同期
- 05スクリーンショットとフォーカスの指針
活用シーン
- 01コンポーザブルの読み込み・エラー分岐のテスト
- 02クリック後のコールバック動作の確認
- 03キーボードによるフォーカス移動の検証
- 04視覚的なレイアウト契約の記録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ chrisbanes/ skills --skill compose-ui-testing-patternsインストール後、Claude Code で/compose-ui-testing-patternsを入力して読み込まれたか確認します。
同梱ファイルreferences/interaction-state.md
詳細
Compose:UI テストのパターン
基本原則
動作を証明するのに必要な、最小限の UI コントラクトをテストします。コールバックを使った、状態に基づくシンプルな UI テストを優先してください。ライフサイクル、ナビゲーション、DI、またはプラットフォームの動作がテスト対象である場合にのみ、統合テストを追加します。
手順
- このタスクで証明するよう求められている動作とテスト対象を明確にします。
- 既存のテストがその対象に対応しているかを確認し、以下の表から必要十分な最小の接点を選びます。
- 変更は依頼されたテスト対象に絞ります。テスト専用のヘルパーを本番コードに移したり、本番コードの境界自体がテスト対象でない限り、本番 API の範囲を広げたりしないでください。
- 制御可能な状態または入力を使い、必要に応じて Compose で同期し、確認可能なセマンティクス、表示結果、またはコールバックの結果を検証します。
- 既存のテストがすでに最も範囲の狭い適切な接点を使い、求められた動作を証明している場合は、変更を加えずに完了します。
テスト対象の選択
| 証明する内容 | テスト形式 |
|---|---|
| テキスト、ボタン、ローディング/エラーの分岐、条件付きコンテンツ | シンプルな UI Compose テスト |
| クリック/入力時のコールバックの接続 | シンプルな UI Compose テスト |
| フォーカス移動またはキーボードの動作 | キー入力を使う Compose テスト |
| 視覚的なレイアウト、クリッピング、エレベーション、タイポグラフィ、画像の構成 | スクリーンショットテスト |
| 状態ホルダーが UI を正しく更新すること | 状態ホルダー/ユニットテストと、接続確認用のスモークテスト 1 件 |
| ホバー、押下、フォーカス、ドラッグのインタラクション状態 | MutableInteractionSource を使ったシンプルな UI テスト |
| ナビゲーション、ライフサイクル、DI の統合 | 統合テスト |
シンプルな UI テストを優先する
画面が状態ホルダーと UI に分かれている場合は、シンプルな UI コンポーザブルをテストします。
composeTestRule.setContent {
ProfileScreen(
state = ProfileUiState(name = "Ada", canSave = true),
onNameChange = {},
onSaveClick = { saved = true },
onBackClick = {},
)
}
composeTestRule.onNodeWithText("Ada").assertIsDisplayed()
composeTestRule.onNodeWithText("Save").performClick()
assertThat(saved).isTrue()
これにより、レイアウトの動作をテストするために ViewModel、コンポーネント、リポジトリ、ナビゲーション、依存関係グラフを構築する必要がなくなります。
まずセマンティクスを確認する
動作がセマンティクスに関するものなら、セマンティクスを検証します。
- テキストが存在する:
onNodeWithText。 - ボタンが有効/無効である:
assertIsEnabled、assertIsNotEnabled。 - コンテンツが選択/フォーカス/切り替えられている:セマンティクスのアサーションを使います。
- コンテンツが存在しない:
assertDoesNotExist。
安定したユーザー可視テキストがないノードや、複数のノードで同じテキストを共有している場合は、テストタグを使います。すべてのアサーションで最初にテストタグを使うのではなく、ユーザー可視のセマンティクスのほうが通常は有効です。
コールバックのテスト
シンプルなカウンターまたはキャプチャした値を使います。
var selectedId: String? = null
composeTestRule.setContent {
ItemList(
items = listOf(ItemUi("movie-1", "Movie")),
onItemClick = { selectedId = it },
)
}
composeTestRule.onNodeWithText("Movie").performClick()
assertThat(selectedId).isEqualTo("movie-1")
単純にキャプチャしたコールバック値は、アクション後に直接アサーションするだけで十分な場合がほとんどです。結果を読み取る前に Compose がスナップショット状態の適用、再コンポジション、またはキューに入った UI 処理を終える必要がある場合は、runOnIdle を使います。
UI テストの決定性を保つ
レイアウト、分岐、コールバックの動作では、本番アプリの依存関係グラフを構築するのではなく、setContent で制御された状態を描画します。本番環境の DI、リポジトリ、ライフサイクルオブザーバー、バックグラウンドエフェクトは、シンプルな UI コントラクトには無関係な非同期処理を追加し、テストを不安定にする可能性があります。
Compose を待機するために Thread.sleep を使わないでください。UI を既知の状態にしてから、セマンティクスのアサーションと Compose の同期機能(waitForIdle、runOnIdle、または実際の非同期条件に対する上限付きの waitUntil)を使います。mainClock.autoAdvance = false を設定した場合は、アサーションの前に、確認したい時点までクロックを進めます(mainClock.advanceTimeBy)。1 つのフレームのチェック中にクロックを進めないでください。それ以外の場合は、自動進行を有効にしたままにします。アプリ全体の統合テストは、ナビゲーション、ライフサイクル、DI、またはプラットフォームとの接続に実際に依存する動作に限って使います。
MutableInteractionSource によるインタラクション状態
インタラクション状態がテスト対象である場合は、テストを作成またはレビューする前に インタラクション状態の手順をすべて確認します。この手順では、不安定なポインター操作のシミュレーションではなく、直接注入したインタラクション状態を使うことが求められます。
キーボードとフォーカス
キーボード、テレビ、デスクトップの UI では、クリックだけでなく、ユーザーが使うのと同じ入力モデル(キー/D-pad)でナビゲーションを操作します。色や拡大率ではなく、フォーカスのセマンティクスを検証します。スクリーンショットはフォーカスの視覚的な表現の確認に使用します。 調査対象の composable に状態を変化させる入力サーフェスがない場合は、 キー操作によるテストを提案する前に、不足しているホストまたは親との インタラクション境界を明示してください。提供された UI にセレクターや コントロールがあると想定してはいけません。
詳細(フォーカスグラフ、FocusRequester、復元、キーイベントハンドラー、テストパターン):compose-focus-navigation。
スクリーンショットテスト
セマンティクスでは証明できない視覚的な契約には、スクリーンショットを使用します。
- レイアウトの間隔/配置。
- テーマの色、タイポグラフィ、エレベーション、シャドウ。
- 画像の構成、グラデーション、オーバーレイ。
- フォーカスハイライトの外観。
- ローディングスケルトンや、視覚的に情報量の多い状態。
スクリーンショットの状態は決定論的に保ちます。
- 固定の状態データを使用します。
- 可能な場合は、時計やアニメーションの進行を固定します。
- ネットワーク/画像の読み込みは、フェイクまたはプレビュー用ハンドラーに置き換えます。
- 制御されていない限り、現在時刻のような動的なテキストの検証は避けます。
スクリーンショットのデフォルト値を変更する場合
- 意図的に変更した名前付きプリセットまたはデフォルト値はテストに維持し、ベースラインを更新します。以前の画像を維持するために生の値に置き換えてはいけません。独立した根拠がない限り、アサーションと許容範囲を維持します。
- 固定の解像度またはジオメトリが契約である場合は、明示的な固定値を維持します。
- 記録と比較を別々に検証します。想定される成果物のパスと、意図したベースラインの差分を調べます。テストから正確な出力パスを読み取り、その成果物が存在することを確認し、検証の証拠にはその文字列どおりのパスを記載します。検証ログに成功と記録されている場合は、何が成功したかを述べたうえで、記録を個別に確認します。比較に成功しても、記録によって成果物が書き込まれた証明にはなりません。成果物またはベースラインの差分を確認できない場合は、記録が成功したと主張せず、その不足を報告します。ツール固有のコマンドはリポジトリのランブックに記載します。指摘のみを行うレビューでは、不足している証拠を述べ、修正案や再実行の手順は追加しません。
フェイク画像とプラットフォームサービス
画像の内容が重要でない場合は、ローダーをフェイクにし、それが動作である場合は要求されたモデルを検証します。正確なフックは画像ライブラリによって異なります。プロジェクトのヘルパーは次のようになります。
val requestedModels = mutableListOf<Any?>()
// Example helper, not a Compose API.
setContentWithFakeImageLoader { request ->
requestedModels += request.data
errorPainter()
}
画像の見た目が重要な場合は、ネットワークデータではなく、決定論的なローカルの painter/ビットマップを指定します。