Skip to content

000 — 测试用例编写规范

1. 概述

本文档定义筷字输入法 v4 版本的单元测试用例编写规范,包括测试用例的编写原则、文档格式、命名约定、覆盖要求和验证标准。所有测试文档和测试代码必须遵循本规范。


2. 核心原则

2.1 职责分离:验收员编写测试

只有软件验收员才能编写和更新单元测试用例。 这是不可逾越的红线,其理由如下:

  • 独立视角:验收员从功能规格出发编写测试,而非从实现细节出发。开发员编写的测试容易受实现思路影响,倾向于验证「代码做了什么」而非「代码应该做什么」,导致测试失去独立检验的价值。
  • 防止自证:开发员自己写测试自己通过,容易陷入「实现即预期」的陷阱——把错误的实现行为当作正确的预期结果写入测试,测试永远通过但功能永远不对。
  • 需求驱动:验收员基于设计文档和验收标准编写测试,确保测试用例反映的是需求规格而非代码结构。测试应回答「功能是否满足设计要求」,而非「代码是否按写好的逻辑运行」。

开发员如果发现测试用例存在问题(如预期结果与设计文档不符、测试数据不合理等),应提交缺陷反馈,由验收员判断并修正测试。开发员不得直接修改测试用例。

2.2 测试必须真正通过

每个测试用例的通过不是纸上谈兵,必须满足以下条件:

  1. 可执行:测试代码已编写完成,可以在测试环境中运行
  2. 可复现:在相同环境下多次执行,结果一致(排除依赖时间、网络等不确定因素的测试)
  3. 实际通过:在当前代码上执行测试,结果为通过(绿色)
  4. 有意义:测试确实验证了有价值的行为,而非空测试或恒真断言

如果测试用例无法通过,存在两种可能:

  • 功能实现有缺陷:开发员修正代码后,验收员重新执行测试
  • 测试设计有缺陷:验收员修正测试用例后重新执行

无论哪种情况,只有测试最终执行通过才能标记为 ✅已通过。

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.md

3.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),测试必须覆盖:

  1. 所有合法状态转换:状态机图中每一条边至少一个测试用例
  2. 非法状态转换:不合法的 Intent 在特定状态下应如何处理(Fail Fast 或忽略)
  3. 状态机边界:初始状态、终态、循环状态(如连续输入)
  4. Intent 参数变化:相同 Intent 在不同参数下的不同行为

7.3 ImeConfig 测试的特殊要求

ImeConfig 的运行时优先语义是核心行为,必须覆盖:

  1. 初始化:应用启动时 ImeConfig 从持久化配置初始化
  2. 运行时修改:运行时修改 ImeConfig 字段后,引擎和 UI 立即响应
  3. 运行时覆盖优先:已被运行时覆盖的字段(runtimeOverrides 记录)不会被持久化同步覆盖
  4. 重启重置:应用重启后,运行时覆盖失效,ImeConfig 重新从持久化配置初始化
  5. 部分覆盖:仅覆盖部分字段时,未覆盖字段仍跟随持久化配置

8. 验证标准

8.1 测试文档完成标准

测试文档视为「编写完成」需满足:

  • [ ] 所有 P0 和 P1 用例已编写
  • [ ] 每个用例的编号、前置条件、测试步骤、预期结果、对应设计章节均已填写
  • [ ] 测试代码已编写并可在本地执行

8.2 测试通过标准

测试文档标记为「✅已通过」需满足:

  • [ ] 所有测试用例在当前代码上执行通过(绿色)
  • [ ] 无跳过的测试用例(除非有明确标注原因且已记录为已知问题)
  • [ ] 测试覆盖了设计文档中定义的核心行为
  • [ ] 验收员已在测试执行记录中填写结果

8.3 验收员验证清单

验收员在标记测试通过前,需逐项确认:

  1. 测试可运行./gradlew test./gradlew :ime-engine:test 全部通过
  2. 断言有意义:测试包含实质性断言(非恒真断言)
  3. 预期结果正确:预期结果与设计文档描述一致,非简单复制实现代码的输出
  4. 无遗漏场景:设计文档中的核心功能点均有对应测试
  5. 隔离性良好:测试间无隐式依赖,可单独运行任意测试

9. 测试执行记录规则

9.1 记录要求

  • 每次执行测试后,验收员必须在测试文档的「测试执行记录」章节追加一轮记录
  • 记录包含每个用例的执行结果、失败原因(如有)、执行时间和执行员
  • 历史轮次不可删除或修改(只追加原则)

9.2 失败处理

测试失败时,验收员需:

  1. 在执行记录中记录失败原因
  2. 判断失败原因属于「功能缺陷」还是「测试设计缺陷」
  3. 如果是功能缺陷:通知开发员修正代码
  4. 如果是测试设计缺陷:验收员修正测试用例
  5. 修正后开启新一轮执行

9.3 轮次编号

轮次编号从 1 开始递增,每轮包含完整的测试执行记录。即使只有部分用例需要重新执行,也记录为新的一轮,包含所有用例的执行结果。


10. 与其他文档的关系

文档类型与测试文档的关系
设计文档(design/测试用例的预期结果来源于设计文档的功能规格描述
计划文档(plans/测试文档与开发计划同名绑定;计划的每个任务需测试通过才算完成
缺陷修复(bugs/测试发现的缺陷记录在 bugs/ 中,修复后通过测试验证
开发日志(logs/测试执行和结果的关键事件可记录在开发日志中

11. 禁止事项

  1. 禁止开发员编写单元测试:开发员可以编写临时调试代码,但不得提交为正式测试用例
  2. 禁止跳过失败的测试:测试失败必须修正,不得使用 @Disabled@Ignore 掩盖问题
  3. 禁止恒真断言:如 assertEquals(true, true)、无断言的测试、只验证异常不被抛出的测试
  4. 禁止修改测试用例以适配错误实现:如果实现与设计不符,应修正实现而非测试
  5. 禁止测试私有方法:测试应验证公开行为,而非内部实现细节。私有方法的正确性应通过公开 API 的测试间接验证