Appearance
000 — 测试用例编写规范
1. 概述
本文档定义筷字输入法 v4 版本的单元测试用例编写规范,包括测试用例的编写原则、文档格式、命名约定、覆盖要求和验证标准。所有测试文档和测试代码必须遵循本规范。
2. 核心原则
2.1 职责分离:验收员编写测试
只有软件验收员才能编写和更新单元测试用例。 这是不可逾越的红线,其理由如下:
- 独立视角:验收员从功能规格出发编写测试,而非从实现细节出发。开发员编写的测试容易受实现思路影响,倾向于验证「代码做了什么」而非「代码应该做什么」,导致测试失去独立检验的价值。
- 防止自证:开发员自己写测试自己通过,容易陷入「实现即预期」的陷阱——把错误的实现行为当作正确的预期结果写入测试,测试永远通过但功能永远不对。
- 需求驱动:验收员基于设计文档和验收标准编写测试,确保测试用例反映的是需求规格而非代码结构。测试应回答「功能是否满足设计要求」,而非「代码是否按写好的逻辑运行」。
开发员如果发现测试用例存在问题(如预期结果与设计文档不符、测试数据不合理等),应提交缺陷反馈,由验收员判断并修正测试。开发员不得直接修改测试用例。
2.2 测试必须真正通过
每个测试用例的通过不是纸上谈兵,必须满足以下条件:
- 可执行:测试代码已编写完成,可以在测试环境中运行
- 可复现:在相同环境下多次执行,结果一致(排除依赖时间、网络等不确定因素的测试)
- 实际通过:在当前代码上执行测试,结果为通过(绿色)
- 有意义:测试确实验证了有价值的行为,而非空测试或恒真断言
如果测试用例无法通过,存在两种可能:
- 功能实现有缺陷:开发员修正代码后,验收员重新执行测试
- 测试设计有缺陷:验收员修正测试用例后重新执行
无论哪种情况,只有测试最终执行通过才能标记为 ✅已通过。
2.3 测试与开发计划绑定
测试文档对应开发计划而非设计文档。开发计划中的每个任务,除了相应的代码和文档等已编写外,相关的单元测试也必须全部测试通过才能标记为已完成。测试文档在开发计划验收时同步更新。
验收员通过执行测试来判定验收是否通过,流程如下:
设计师编写设计文档
↓
设计师创建开发计划(定义任务和验收标准)
↓
验收员根据开发计划的验收标准编写测试用例
↓
开发员根据设计文档实现功能代码
↓
验收员执行测试用例
├─ 通过 → 任务标记为已完成
└─ 未通过 → 反馈失败信息,开发员修正代码,验收员重新执行3. 测试文档格式
3.1 文档命名
测试文档与对应的开发计划同名,后缀为 -tests.md。测试文档编号与开发计划编号一致:
{计划编号}-{计划名称}-tests.md ← 对应 plans/{计划编号}-{计划名称}.md示例:
001-keyboard-state-machine-tests.md ← 对应 plans/001-keyboard-state-machine.md
002-ime-engine-api-tests.md ← 对应 plans/002-ime-engine-api.md
003-input-list-tests.md ← 对应 plans/003-input-list.md3.2 文档结构模版
每个测试文档必须遵循以下结构:
markdown
# {编号} — {测试范围名称}测试
## 基本信息
| 字段 | 值 |
|------|-----|
| 对应开发计划 | {引用 plans/ 中的计划编号和名称} |
| 关联设计文档 | {引用 design/ 中的相关文档编号和名称} |
| 测试范围 | {简述本测试覆盖的功能范围} |
| 编写员 | 软件验收员 |
| 创建时间 | {YYYY-MM-DD HH:mm} |
| 更新时间 | {YYYY-MM-DD HH:mm} |
| 测试状态 | 📋待编写 / 🔄编写中 / ✅已通过 / ❌未通过 / ⏸️暂停 |
---
## 测试概览
### 测试目标
{简述本测试文档的目标,即验证哪些功能行为}
### 测试策略
{说明测试方法:单元测试、集成测试、参数化测试等}
### 测试代码位置
{测试代码在源码中的目录路径}
---
## 测试用例
### TC-{编号}-001: {测试用例名称}
| 字段 | 值 |
|------|-----|
| 用例编号 | TC-{编号}-001 |
| 所属模块 | {模块名称} |
| 优先级 | P0(冒烟)/ P1(核心)/ P2(常规)/ P3(边界) |
| 前置条件 | {执行此用例前需要满足的条件} |
| 测试步骤 | 1. {步骤 1} 2. {步骤 2} ... |
| 预期结果 | {明确的预期行为描述} |
| 对应设计 | {引用设计文档的具体章节} |
| 测试方法 | {Kotlin 测试函数名} |
### TC-{编号}-002: {测试用例名称}
...(后续用例按相同格式)
---
## 测试执行记录
> **重要**:测试执行记录由软件验收员填写。每次执行测试后,记录执行结果。
### 第 1 轮
| 用例编号 | 执行结果 | 失败原因(如有) | 执行时间 | 执行员 |
|----------|----------|-----------------|----------|--------|
| TC-{编号}-001 | ✅通过 / ❌失败 | {失败原因} | {时间} | 验收员 |
| TC-{编号}-002 | ✅通过 / ❌失败 | {失败原因} | {时间} | 验收员 |
...(后续轮次按相同格式追加)4. 测试用例编号规则
测试用例编号格式:TC-{计划编号}-{序号}
- 计划编号:三位数字,与对应的开发计划编号一致
- 序号:三位数字,从 001 开始递增
示例:
| 编号 | 说明 |
|---|---|
| TC-001-001 | 键盘状态机开发计划的第 1 个测试用例 |
| TC-002-015 | 引擎库 API 开发计划的第 15 个测试用例 |
| TC-005-003 | 配置系统开发计划的第 3 个测试用例 |
5. 优先级定义
| 优先级 | 名称 | 说明 | 必须覆盖 |
|---|---|---|---|
| P0 | 冒烟测试 | 最基本的功能验证,失败意味着核心功能不可用 | ✅ |
| P1 | 核心功能 | 主要功能路径的正常流程和常见异常流程 | ✅ |
| P2 | 常规功能 | 次要功能、配置变更、边界值 | 视情况 |
| P3 | 边界与异常 | 极端边界条件、异常输入、并发场景 | 视情况 |
每个测试文档必须覆盖所有 P0 和 P1 用例。P2 和 P3 用例根据模块重要性决定是否编写。
6. 测试代码规范
6.1 命名约定
测试类名与被测类名对应,后缀为 Test:
kotlin
// 被测类:ImeEngine
// 测试类:ImeEngineTest
// 被测类:KeyboardStateMachine
// 测试类:KeyboardStateMachineTest测试函数名使用 should_预期行为_when_触发条件 格式,或使用 Kotlin 反引号描述性命名:
kotlin
@Test
fun `should transition to InputWaiting when PressKey intent received in Idle state`() {
// ...
}
@Test
fun `should throw IllegalStateException when accessing disabled feature`() {
// ...
}
@Test
fun `should return empty candidates when pinyin query has no match`() {
// ...
}6.2 测试结构:Arrange-Act-Assert
每个测试函数遵循 AAA 模式:
kotlin
@Test
fun `should commit text when CommitInput intent processed with non-empty input list`() {
// Arrange:准备测试数据和环境
val engine = ImeEngine.create(
config = ImeConfig(),
dictProvider = ImeInMemoryDictProvider(testPinyinData),
)
engine.handleIntent(ImeIntent.PressKey(key = InputKey.Char('n'), gesture = KeyGesture.Tap))
engine.handleIntent(ImeIntent.PressKey(key = InputKey.Char('i'), gesture = KeyGesture.Tap))
// Act:执行被测行为
engine.handleIntent(ImeIntent.CommitInput)
// Assert:验证结果
val output = engine.output.tryReceive().getOrNull()
assertNotNull(output)
assertEquals(ImeOutput.CommitText::class, output!!::class)
assertEquals("你", (output as ImeOutput.CommitText).text)
}6.3 测试隔离
- 每个测试函数独立运行,不依赖其他测试的执行顺序
- 使用
@BeforeEach/@AfterEach初始化和清理测试环境 - 不依赖共享的可变状态
- 需要外部依赖时使用 Mock 或内存实现(如
ImeInMemoryDictProvider),不依赖真实数据库或文件系统
6.4 Mock 使用原则
- 优先使用真实实现:对于纯逻辑组件(状态机、InputList、ImeConfig),直接使用真实实现而非 Mock
- Mock 仅用于外部依赖:数据库、文件系统、系统服务等无法在单元测试中使用的依赖才使用 Mock
- 使用接口而非 Mock 库:对于字典等可替换组件,优先使用
ImeInMemoryDictProvider等测试专用实现,而非 Mockito 等 Mock 框架 - 不 Mock 被测类自身:被测类的内部方法不应被 Mock,否则测试失去意义
kotlin
// ✅ 正确:使用内存实现替代数据库
val dictProvider = ImeInMemoryDictProvider(mapOf("ni" to listOf(testWord)))
// ❌ 错误:Mock 被测类的内部方法
val engine = mockk<ImeEngine> {
every { reduce(any(), any()) } returns testState
}7. 覆盖要求
7.1 必须覆盖的场景
每个模块的测试至少覆盖以下场景类型:
| 场景类型 | 说明 | 示例 |
|---|---|---|
| 正常路径 | 功能在标准输入下的预期行为 | 输入拼音 → 获得候选 → 选择候选 → 提交文本 |
| 边界条件 | 极端或临界输入 | 空输入列表提交、候选列表为空时翻页、光标在首尾位置移动 |
| 状态转换 | 状态机所有合法转换路径 | Idle→InputWaiting、InputWaiting→Sliding、Sliding→InputWaiting |
| 非法输入 | 不合法的操作或参数 | 禁用功能后调用相关 Intent、空字符串查询候选 |
| 配置变更 | 配置修改对行为的影响 | 左右手模式切换、禁用剪贴板功能、运行时覆盖优先于持久化 |
| 并发安全 | 多线程访问场景(如适用) | 输入列表在主线程读取、协程中异步更新 |
7.2 状态机测试的完整覆盖
对于键盘状态机(设计文档 100),测试必须覆盖:
- 所有合法状态转换:状态机图中每一条边至少一个测试用例
- 非法状态转换:不合法的 Intent 在特定状态下应如何处理(Fail Fast 或忽略)
- 状态机边界:初始状态、终态、循环状态(如连续输入)
- Intent 参数变化:相同 Intent 在不同参数下的不同行为
7.3 ImeConfig 测试的特殊要求
ImeConfig 的运行时优先语义是核心行为,必须覆盖:
- 初始化:应用启动时 ImeConfig 从持久化配置初始化
- 运行时修改:运行时修改 ImeConfig 字段后,引擎和 UI 立即响应
- 运行时覆盖优先:已被运行时覆盖的字段(
runtimeOverrides记录)不会被持久化同步覆盖 - 重启重置:应用重启后,运行时覆盖失效,ImeConfig 重新从持久化配置初始化
- 部分覆盖:仅覆盖部分字段时,未覆盖字段仍跟随持久化配置
8. 验证标准
8.1 测试文档完成标准
测试文档视为「编写完成」需满足:
- [ ] 所有 P0 和 P1 用例已编写
- [ ] 每个用例的编号、前置条件、测试步骤、预期结果、对应设计章节均已填写
- [ ] 测试代码已编写并可在本地执行
8.2 测试通过标准
测试文档标记为「✅已通过」需满足:
- [ ] 所有测试用例在当前代码上执行通过(绿色)
- [ ] 无跳过的测试用例(除非有明确标注原因且已记录为已知问题)
- [ ] 测试覆盖了设计文档中定义的核心行为
- [ ] 验收员已在测试执行记录中填写结果
8.3 验收员验证清单
验收员在标记测试通过前,需逐项确认:
- 测试可运行:
./gradlew test或./gradlew :ime-engine:test全部通过 - 断言有意义:测试包含实质性断言(非恒真断言)
- 预期结果正确:预期结果与设计文档描述一致,非简单复制实现代码的输出
- 无遗漏场景:设计文档中的核心功能点均有对应测试
- 隔离性良好:测试间无隐式依赖,可单独运行任意测试
9. 测试执行记录规则
9.1 记录要求
- 每次执行测试后,验收员必须在测试文档的「测试执行记录」章节追加一轮记录
- 记录包含每个用例的执行结果、失败原因(如有)、执行时间和执行员
- 历史轮次不可删除或修改(只追加原则)
9.2 失败处理
测试失败时,验收员需:
- 在执行记录中记录失败原因
- 判断失败原因属于「功能缺陷」还是「测试设计缺陷」
- 如果是功能缺陷:通知开发员修正代码
- 如果是测试设计缺陷:验收员修正测试用例
- 修正后开启新一轮执行
9.3 轮次编号
轮次编号从 1 开始递增,每轮包含完整的测试执行记录。即使只有部分用例需要重新执行,也记录为新的一轮,包含所有用例的执行结果。
10. 与其他文档的关系
| 文档类型 | 与测试文档的关系 |
|---|---|
设计文档(design/) | 测试用例的预期结果来源于设计文档的功能规格描述 |
计划文档(plans/) | 测试文档与开发计划同名绑定;计划的每个任务需测试通过才算完成 |
缺陷修复(bugs/) | 测试发现的缺陷记录在 bugs/ 中,修复后通过测试验证 |
开发日志(logs/) | 测试执行和结果的关键事件可记录在开发日志中 |
11. 禁止事项
- 禁止开发员编写单元测试:开发员可以编写临时调试代码,但不得提交为正式测试用例
- 禁止跳过失败的测试:测试失败必须修正,不得使用
@Disabled或@Ignore掩盖问题 - 禁止恒真断言:如
assertEquals(true, true)、无断言的测试、只验证异常不被抛出的测试 - 禁止修改测试用例以适配错误实现:如果实现与设计不符,应修正实现而非测试
- 禁止测试私有方法:测试应验证公开行为,而非内部实现细节。私有方法的正确性应通过公开 API 的测试间接验证