关于 testing-android-code
面向 Bitwarden Android 代码贡献者的测试技能,整理项目专用测试模式。内容涵盖测试基类、Flow 模式、测试数据构造器和常见问题。
主要功能
- 01ViewModel 测试设置
- 02Compose UI 测试模式
- 03服务与 Repository 测试
- 04测试数据构造器
- 05Flow 与 Result 断言
适用场景
- 01测试 ViewModel 状态与事件
- 02测试 Compose 屏幕
- 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.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
优点:
- 自动注册
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.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. 测试实用工具和辅助函数
Fake 实现
| Fake | 位置 | 用途 |
|---|---|---|
FakeDispatcherManager | core/src/testFixtures/ | 确定性协程执行 |
FakeConfigDiskSource | data/src/testFixtures/ | 内存中的配置存储 |
FakeSharedPreferences | data/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 特有的关键测试模式:
- BaseViewModelTest - 自动设置调度器,并提供
stateEventFlow()辅助函数 - BitwardenComposeTest - 预先配置所有管理器和主题
- BaseServiceTest - 为网络测试设置 MockWebServer
- Turbine Flow 测试 - StateFlow(重放)与 EventFlow(不重放)
- 测试数据构建器 - 统一使用
number: Int参数模式 - Fake 实现 - 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- 包含 Mock 和 Fake 的完整仓库测试