testing-android-code について
Bitwarden の Android コードに取り組む開発者向けのテストスキルです。テスト基底クラス、Flow のパターン、フィクスチャ、よくある注意点をまとめています。
主な機能
- 01ViewModel のテスト設定
- 02Compose UI テストパターン
- 03サービスと Repository のテスト
- 04テストデータビルダー
- 05Flow と Result のアサーション
活用シーン
- 01ViewModel の状態とイベントのテスト
- 02Compose 画面のテスト
- 03MockWebServer によるサービス応答のテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bitwarden/ android --skill testing-android-codeインストール後、Claude Code で/testing-android-codeを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
examples/compose-screen-test-example.mdrepository-test-example.mdviewmodel-test-example.mdreferences/critical-gotchas.mdflow-testing-patterns.mdtest-base-classes.md詳細
Android コードのテスト - Bitwarden のテストパターン
このスキルでは、Bitwarden 固有のパターンに関する実践的なテストガイダンスを提供します。 アーキテクチャとテストに関する理念の詳細については、docs/ARCHITECTURE.md を参照してください。
テストフレームワークの設定
必須の依存関係:
- JUnit 5(jupiter)、MockK、Turbine(app.cash.turbine)
- kotlinx.coroutines.test、Robolectric、Compose Test
重要な注意: 一貫性を保つため、テストは en-US ロケールで実行されます。他のロケールを想定しないでください。
A. ViewModel のテストパターン
基底クラス:BaseViewModelTest
ViewModel のテストでは、必ず BaseViewModelTest を継承してください。
場所: ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt
利点:
UnconfinedTestDispatcher用のMainDispatcherExtensionを自動的に登録します- StateFlow と EventFlow を同時にテストするための
stateEventFlow()ヘルパーを提供します
パターン:
class ExampleViewModelTest : BaseViewModelTest() {
private val mockRepository: ExampleRepository = mockk()
private val savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to INITIAL_STATE))
@Test
fun `ButtonClick should fetch data and update state`() = runTest {
coEvery { mockRepository.fetchData(any()) } returns Result.success("data")
val viewModel = ExampleViewModel(savedStateHandle, mockRepository)
viewModel.stateFlow.test {
assertEquals(INITIAL_STATE, awaitItem())
viewModel.trySendAction(ExampleAction.ButtonClick)
assertEquals(INITIAL_STATE.copy(data = "data"), awaitItem())
}
coVerify { mockRepository.fetchData(any()) }
}
}
完全な例: references/test-base-classes.md を参照してください
StateFlow と EventFlow の違い(重要)
| Flow の種類 | リプレイ | 最初のアクション | パターン |
|---|---|---|---|
| StateFlow | あり(1) | awaitItem() で現在の状態を取得 | 初期状態を確認 → トリガー → 新しい状態を確認 |
| EventFlow | なし | 最初に expectNoEvents() を呼び出す | expectNoEvents → トリガー → イベントを確認 |
詳細なパターン: references/flow-testing-patterns.md を参照してください
B. Compose UI のテストパターン
基底クラス:BitwardenComposeTest
Compose 画面のテストでは、必ず BitwardenComposeTest を継承してください。
場所: app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt
利点:
- Bitwarden のすべてのマネージャー(FeatureFlags、AuthTab、Biometrics など)を事前に設定します
- コンテンツを
BitwardenThemeとLocalManagerProviderでラップします - 時間に基づくテストで決定性を確保するため、固定 Clock を提供します
パターン:
class ExampleScreenTest : BitwardenComposeTest() {
private var haveCalledNavigateBack = false
private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<ExampleViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setup() {
setContent {
ExampleScreen(
onNavigateBack = { haveCalledNavigateBack = true },
viewModel = viewModel,
)
}
}
@Test
fun `on back click should send BackClick action`() {
composeTestRule.onNodeWithContentDescription("Back").performClick()
verify { viewModel.trySendAction(ExampleAction.BackClick) }
}
}
注意: Compose テストでイベントをテストするには、bufferedMutableSharedFlow を使用してください。デフォルトのリプレイ数は 0 です。必要に応じて replay = 1 を渡してください。
基底クラスの詳細: references/test-base-classes.md を参照してください
C. リポジトリとサービスのテスト
MockWebServer を使用したサービスのテスト
基底クラス: BaseServiceTest(network/src/testFixtures/)
class ExampleServiceTest : BaseServiceTest() {
private val api: ExampleApi = retrofit.create()
private val service = ExampleServiceImpl(api)
@Test
fun `getConfig should return success when API succeeds`() = runTest {
server.enqueue(MockResponse().setBody(EXPECTED_JSON))
val result = service.getConfig()
assertEquals(EXPECTED_RESPONSE.asSuccess(), result)
}
}
リポジトリのテストパターン
class ExampleRepositoryTest {
private val fixedClock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val dispatcherManager = FakeDispatcherManager()
private val mockDiskSource: ExampleDiskSource = mockk()
private val mockService: ExampleService = mockk()
private val repository = ExampleRepositoryImpl(
clock = fixedClock,
exampleDiskSource = mockDiskSource,
exampleService = mockService,
dispatcherManager = dispatcherManager,
)
@Test
fun `fetchData should return success when service succeeds`() = runTest {
coEvery { mockService.getData(any()) } returns expectedData.asSuccess()
val result = repository.fetchData(userId)
assertTrue(result.isSuccess)
}
}
主なパターン: FakeDispatcherManager、固定 Clock、.asSuccess() ヘルパーを使用します。
D. テストデータビルダー
数値パラメーターを使ったビルダーパターン
場所: network/src/testFixtures/kotlin/com/bitwarden/network/model/
fun createMockCipher(
number: Int,
id: String = "mockId-$number",
name: String? = "mockName-$number",
// ... more parameters with defaults
): SyncResponseJson.Cipher
// Usage:
val cipher1 = createMockCipher(number = 1) // mockId-1, mockName-1
val cipher2 = createMockCipher(number = 2) // mockId-2, mockName-2
val custom = createMockCipher(number = 3, name = "Custom")
使用可能なビルダー(35 個以上):
- Cipher:
createMockCipher()、createMockLogin()、createMockCard()、createMockIdentity()、createMockSecureNote()、createMockSshKey()、createMockField()、createMockUri()、createMockFido2Credential()、createMockPasswordHistory()、createMockCipherPermissions() - 同期:
createMockSyncResponse()、createMockFolder()、createMockCollection()、createMockPolicy()、createMockDomains() - Send:
createMockSend()、createMockFile()、createMockText()、createMockSendJsonRequest() - プロフィール:
createMockProfile()、createMockOrganization()、createMockProvider()、createMockPermissions() - 添付ファイル:
createMockAttachment()、createMockAttachmentJsonRequest()、createMockAttachmentResponse()
一覧については、network/src/testFixtures/kotlin/com/bitwarden/network/model/ を参照してください。
E. Result 型のテスト
場所:
.asSuccess()、.asFailure:core/src/main/kotlin/com/bitwarden/core/data/util/ResultExtensions.ktassertCoroutineThrows:core/src/testFixtures/kotlin/com/bitwarden/core/data/util/TestHelpers.kt
// Create results
"data".asSuccess() // Result.success("data")
throwable.asFailure() // Result.failure<T>(throwable)
// Assertions
assertTrue(result.isSuccess)
assertEquals(expectedValue, result.getOrNull())
F. テスト用ユーティリティとヘルパー
フェイク実装
| フェイク | 場所 | 目的 |
|---|---|---|
FakeDispatcherManager | core/src/testFixtures/ | 決定論的なコルーチン実行 |
FakeConfigDiskSource | data/src/testFixtures/ | インメモリの設定ストレージ |
FakeSharedPreferences | data/src/testFixtures/ | メモリベースの SharedPreferences |
例外テスト (CRITICAL)
// CORRECT - Call directly, NOT inside runTest
@Test
fun `test exception`() {
assertCoroutineThrows<IllegalStateException> {
repository.throwingFunction()
}
}
理由: runTest は例外をキャッチして再スローするため、アサーションパターンが壊れます。
G. 重要な落とし穴
Bitwarden における一般的なテストの間違い。完全な詳細と例については: references/critical-gotchas.md を参照してください。
コアパターン:
- assertCoroutineThrows + runTest -
runTestでラップせず、直接呼び出してください - 静的モックのクリーンアップ -
@Afterで必ずunmockkStatic()を実行してください - StateFlow と EventFlow - StateFlow: 最初に
awaitItem()、EventFlow: 最初にexpectNoEvents() - FakeDispatcherManager - 実際の
DispatcherManagerImplの代わりに必ず使用してください - コルーチンテストラッパー - すべての Flow/コルーチンテストで
runTest { }を使用してください
アサーションパターン:
- 完全な状態のアサーション - 個々のフィールドではなく、状態オブジェクト全体をアサートしてください
- Kotlin より JUnit - Kotlin の
assert()ではなく、assertTrue()を使用してください - Result 拡張機能を使用する - Result 型のアサーションには
asSuccess()とasFailure()を使用してください
テスト設計:
- フェイクとモックの戦略 - ハッピーパスにはフェイク、エラーパスにはモックを使用してください
- 静的モックより DI - mockkStatic の代わりにインターフェース (UuidManager など) を抽出してください
- Null ストリームのテスト - ContentResolver 操作からの null 戻り値をテストしてください
- bufferedMutableSharedFlow - フェイク内で
.onSubscription { emit(state) }と共に使用してください - テストファクトリメソッド - SavedStateHandle ではなく、ドメイン状態型を受け入れてください
H. テストファイルの構成
ディレクトリ構造
module/src/test/kotlin/com/bitwarden/.../
├── ui/*ScreenTest.kt, *ViewModelTest.kt
├── data/repository/*RepositoryTest.kt
└── network/service/*ServiceTest.kt
module/src/testFixtures/kotlin/com/bitwarden/.../
├── util/TestHelpers.kt
├── base/Base*Test.kt
└── model/*Util.kt
テスト定数の配置
テスト定数は、クラスの閉じ波括弧の後、ファイルの最後にトップレベルの private const val として宣言してください。テスト定数に companion object を使用しないでください。
テストの命名
- クラス:
*Test.kt、*ScreenTest.kt、*ViewModelTest.kt - 関数:
`given state when action should result`
まとめ
Bitwarden 固有の主要なテストパターン:
- BaseViewModelTest -
stateEventFlow()ヘルパーによる自動ディスパッチャー設定 - BitwardenComposeTest - すべてのマネージャーとテーマがあらかじめ設定済み
- BaseServiceTest - ネットワークテスト用の MockWebServer 設定
- Turbine Flow テスト - StateFlow (リプレイあり) と EventFlow (リプレイなし)
- テストデータビルダー - 一貫した
number: Intパラメータパターン - フェイク実装 - FakeDispatcherManager、FakeConfigDiskSource
- Result 型のテスト -
.asSuccess()、.asFailure()
必ず参照してください: docs/ARCHITECTURE.md および既存のテストファイルを参照実装として確認してください。
リファレンスドキュメント
詳細については、以下を参照してください:
references/test-base-classes.md- 基底クラスの詳細なドキュメントと使用パターンreferences/flow-testing-patterns.md- StateFlow/EventFlow の完全な Turbine パターンreferences/critical-gotchas.md- 完全なアンチパターンリファレンスとデバッグのヒント
完全な例:
examples/viewmodel-test-example.md- StateFlow/EventFlow を使用した完全な ViewModel テストexamples/compose-screen-test-example.md- 完全な Compose 画面テストexamples/repository-test-example.md- モックとフェイクを使用した完全なリポジトリテスト