mcpField
T
testing-android-code

testing-android-code

github.com/bitwardenテスト・品質9.6k76 回インストール今日更新
リポジトリ

testing-android-code について

Bitwarden の Android コードに取り組む開発者向けのテストスキルです。テスト基底クラス、Flow のパターン、フィクスチャ、よくある注意点をまとめています。

主な機能

  1. 01ViewModel のテスト設定
  2. 02Compose UI テストパターン
  3. 03サービスと Repository のテスト
  4. 04テストデータビルダー
  5. 05Flow と Result のアサーション

活用シーン

  1. 01ViewModel の状態とイベントのテスト
  2. 02Compose 画面のテスト
  3. 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.md
repository-test-example.md
viewmodel-test-example.md
references/
critical-gotchas.md
flow-testing-patterns.md
test-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.kt
  • assertCoroutineThrows: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. テスト用ユーティリティとヘルパー

フェイク実装
フェイク場所目的
FakeDispatcherManagercore/src/testFixtures/決定論的なコルーチン実行
FakeConfigDiskSourcedata/src/testFixtures/インメモリの設定ストレージ
FakeSharedPreferencesdata/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 固有の主要なテストパターン:

  1. BaseViewModelTest - stateEventFlow() ヘルパーによる自動ディスパッチャー設定
  2. BitwardenComposeTest - すべてのマネージャーとテーマがあらかじめ設定済み
  3. BaseServiceTest - ネットワークテスト用の MockWebServer 設定
  4. Turbine Flow テスト - StateFlow (リプレイあり) と EventFlow (リプレイなし)
  5. テストデータビルダー - 一貫した number: Int パラメータパターン
  6. フェイク実装 - FakeDispatcherManager、FakeConfigDiskSource
  7. 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 - モックとフェイクを使用した完全なリポジトリテスト

コメント