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. 01测试 ViewModel 状态与事件
  2. 02测试 Compose 屏幕
  3. 03使用 MockWebServer 测试服务响应

安装

复制下面的命令,在终端中运行。

$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

优点:

  • 自动注册 MainDispatcherExtension,以使用 UnconfinedTestDispatcher
  • 提供 stateEventFlow() 辅助函数,用于同时测试 StateFlow 和 EventFlow

模式:

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. Repository 和 Service 测试

使用 MockWebServer 测试 Service

基类: 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)
    }
}
Repository 测试模式
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()
  • Sync: createMockSyncResponse()、createMockFolder()、createMockCollection()、createMockPolicy()、createMockDomains()
  • Send: createMockSend()、createMockFile()、createMockText()、createMockSendJsonRequest()
  • Profile: createMockProfile()、createMockOrganization()、createMockProvider()、createMockPermissions()
  • Attachments: 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. 测试实用工具和辅助函数

Fake 实现
Fake位置用途
FakeDispatcherManagercore/src/testFixtures/确定性协程执行
FakeConfigDiskSourcedata/src/testFixtures/内存中的配置存储
FakeSharedPreferencesdata/src/testFixtures/基于内存的 SharedPreferences
异常测试(关键)
// CORRECT - Call directly, NOT inside runTest
@Test
fun `test exception`() {
    assertCoroutineThrows<IllegalStateException> {
        repository.throwingFunction()
    }
}

原因: runTest 会捕获并重新抛出异常,破坏断言模式。


G. 关键注意事项

Bitwarden 中常见的测试错误。完整详情和示例: 请参阅 references/critical-gotchas.md

核心模式:

  • assertCoroutineThrows + runTest - 切勿将其包装在 runTest 中;直接调用
  • 静态 Mock 清理 - 始终在 @After 中调用 unmockkStatic()
  • StateFlow 与 EventFlow - StateFlow:先调用 awaitItem();EventFlow:先调用 expectNoEvents()
  • FakeDispatcherManager - 始终使用它,而不是实际的 DispatcherManagerImpl
  • 协程测试包装器 - 对所有 Flow/协程测试使用 runTest { }

断言模式:

  • 完整状态断言 - 断言整个状态对象,而非单个字段
  • 优先使用 JUnit 而非 Kotlin - 使用 assertTrue(),而不是 Kotlin 的 assert()
  • 使用 Result 扩展函数 - 对 Result 类型断言使用 asSuccess() 和 asFailure()

测试设计:

  • Fake 与 Mock 策略 - 正常路径使用 Fake,错误路径使用 Mock
  • 优先使用 DI 而非静态 Mock - 提取接口(例如 UuidManager),而不是使用 mockkStatic
  • 空流测试 - 测试 ContentResolver 操作返回 null 的情况
  • bufferedMutableSharedFlow - 在 Fake 中与 .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. Fake 实现 - 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 - 包含 Mock 和 Fake 的完整仓库测试

评论

支持 Markdown