关于 design-testing-strategy
用于按工件的关键程度和行为选择测试类型、设计测试用例的参考技能。它结合顺序决策门、跳过启发式、矩阵规范和完整示例。
主要功能
- 01按顺序评估测试决策门
- 02基于关键程度的跳过启发式
- 03测试矩阵填写规范
- 04ISTQB 用例设计技术
- 05端到端示例
适用场景
- 01为代码变更选择测试类型
- 02跳过收益较低的测试
- 03从边界和状态推导用例
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ neolabhq/ context-engineering-kit --skill design-testing-strategy安装后在 Claude Code 中输入/design-testing-strategy确认已加载。
详细介绍
设计测试策略
一份用于设计适合目标、匹配关键性需求的测试策略的参考手册。
这项技能以决策为导向,而非空谈理念:每个关卡都有确定的条件(满足 X 时开启/满足 Y 时关闭),每种架构都必须严格遵循(字段顺序很重要),每个示例都提供端到端的完整演示。
如何使用这项技能
- 按顺序阅读决策关卡(关卡 0 → 关卡 6)。每个关卡相互独立——最终开启的测试类型可以是任意子集。
- 应用策略性跳过启发规则,移除那些针对当前工件投资回报率较低的已开启关卡。
- 对每个已开启的关卡,填写测试矩阵架构(
selected_types条目)——字段顺序至关重要。 - 在
rejected_types中列出已拒绝的类型,在deliberately_skipped中列出有意跳过的类型。 - 使用用例设计技术中的 ISTQB 技术,生成 待覆盖的测试用例 Markdown 项目符号列表。
- 对照对应的完整示例进行交叉检查(A 纯函数/B HTTP+DB 端点/C UI 组件)。
决策门
按数字顺序应用各项门槛。每项门槛都会独立生成一个布尔值(applies: true|false)。各项门槛不会相互否决——单个产物可以同时启用单元测试、集成测试、契约测试和基于属性的测试。
| # | 类型 | 启用条件 | 禁用条件 | 来源 |
|---|---|---|---|---|
| 0 | 全部跳过 | 重要性为 NONE(仅文档、注释、格式调整、生成代码、不含逻辑的配置、一次性原型) | 任何包含分支、计算输出、副作用或用户可见行为的内容 | 《程序员修炼之道》——“严谨且有效地测试”意味着在投资回报为零时有效地跳过测试 |
| 1 | 单元测试 | 代码包含任何逻辑:分支、循环、条件判断、计算、转换、解析、验证、格式化 | 纯声明式接线(DI 注册、路由表),且不包含行为 | 测试金字塔(Vocke) 的基础层 + Beck 的 TDD 红-绿-重构单元测试 |
| 2 | 集成测试 | 跨越边界:HTTP 调用、数据库查询、外部 SDK、消息队列、文件系统 I/O,或与 >=2 个不同协作者协作,且单元测试替身会扭曲行为 | 无 I/O 且仅有 0-1 个稳定协作者的纯函数 | 测试奖杯(Dodds)——集成测试是投资回报最高的层;Google“跟随用户” |
| 3 | 组件测试或端到端测试 | 存在 UI 界面,且重要性 >= MEDIUM-HIGH,且属于面向用户的关键路径(注册、结账、身份验证、支付、主要 CTA) | 仅供内部管理员使用的屏幕、开发工具或非关键 UI | 测试金字塔顶部 + ISO/IEC/IEEE 29119 风险排名 + Google 端到端测试原则 |
| 4 | 契约测试 | 公共 API 被 >=1 个不同客户端使用(移动端 + Web、多个内部服务、外部合作伙伴),且部署节奏相互独立 | 消费方和提供方一起部署的 API | Pact / CDC + Pactflow 对 CDC 的说明 |
| 5 | 冒烟测试 | 存在可部署的界面(Web 应用、API、服务),且有部署/CI 流水线,在部署后进行验证有意义 | 库、内部辅助工具,或没有部署流水线 | Google“怎样才算好的端到端测试”——冒烟测试 = 用于部署门禁的最小端到端测试 |
| 6 | 基于属性的测试 | 输入域较大或无界(数值范围、字符串、列表、解析器、序列化器、编码器、数学),且不变量稳定(往返、幂等性、单调性、交换性),且重要性 >= MEDIUM-HIGH | 输入域有限且较小、不变量不稳定,或重要性为 LOW | Hypothesis / QuickCheck |
门槛应用算法
for gate in [Gate 0, Gate 1, ..., Gate 6]:
if gate.ON_condition_met(artifact):
result[gate.type] = applies: true
else:
result[gate.type] = applies: false
if Gate 0 is true:
short-circuit: emit empty selected_types, document criticality=NONE, stop
重要性等级(供门槛 3 和 6 使用):
| 等级 | 定义 |
|---|---|
NONE | 文档、格式调整、生成代码、一次性代码、不含逻辑的配置 |
LOW | 内部开发工具、仅供管理员使用的屏幕、日志格式化器 |
MEDIUM | 标准 CRUD、由单个团队使用的内部 API、非关键 UI、辅助工具和实用程序 |
MEDIUM-HIGH | 关键路径上的面向用户 UI、由多个使用方调用的公共 API、业务工作流 |
HIGH | 资金转移、身份验证/授权决策、关键安全验证、数据完整性、受监管领域 |
测试类型参考
| 类型 | 适用场景 | 不适用场景 | 框架 | 典型依赖项 | Google 规模 |
|---|---|---|---|---|---|
| unit | 纯逻辑、单个函数/方法/类、确定性输入 | 代码只是没有逻辑的 I/O 编排 | vitest、jest、pytest、go test、JUnit、xUnit、RSpec | 无(或使用内存中的伪对象) | Small |
| integration | 跨越边界(数据库、HTTP、队列、文件系统);涉及多个协作者,且模拟会扭曲行为 | 不涉及边界的纯函数 | vitest、jest、pytest、go test、JUnit + Testcontainers、supertest、TestRestTemplate | 通过 Testcontainers 使用真实的 Postgres/Redis/Kafka、进程内 HTTP 服务、临时目录中的真实文件系统 | Medium(单机,允许 localhost) |
| component | 单个组件内的 UI 渲染和交互,无需完整应用上下文 | 仅后端逻辑;多页面用户流程 | React Testing Library、Vue Test Utils、Angular TestBed、Storybook 交互测试 | jsdom 或 happy-dom,在 fetch/axios 层模拟网络 | Small 到 Medium |
| e2e | 通过运行中的应用完成完整用户路径:真实浏览器、真实后端、真实数据库 | 内部辅助函数、单个组件、非关键 UI | Playwright、Cypress、Selenium | 真实运行中的应用 + 由 Testcontainers 支持的数据库或预置数据的预发布环境 | Large(多进程,可能跨多台机器) |
| smoke | 部署后判断是否上线:访问 /,检查关键端点是否有响应、登录是否可用 | 详细的正确性验证;冒烟测试有意设计得较浅 | Playwright(1-3 条关键路径)、HTTP 探测脚本、k6 最小场景 | 真实部署环境 | Large |
| contract | 公共 API 被 2 个或更多部署节奏独立的不同客户端使用 | 单一使用方的内部 API;提供方和使用方一起部署 | Pact、Spring Cloud Contract、OpenAPI 架构验证器 | Pact broker 或仓库中的契约文件 | Medium |
| property-based | 输入域大或无界,且不变量稳定(解析器、序列化器、编码器、数学运算) | 输入空间小且有限;不变量不稳定 | Hypothesis(Python)、fast-check(TS)、QuickCheck(Haskell)、jqwik(Java)、proptest(Rust) | 与 unit 相同 | Small |
Google 测试规模映射
Google Test Sizes (Bland) 和 SWE at Google Ch.11 根据资源(规模)对测试进行分类,与范围(覆盖的路径)无关:
| 规模 | 进程模型 | 网络 | 文件系统 | 时间预算 | 备注 |
|---|---|---|---|---|---|
small | 单进程、单线程 | 无 | 无(仅内存) | < 100ms | 快速、隔离、可并行 |
medium | 单机,允许多个进程 | 仅 localhost | 允许使用 tmpdir | < 1s | Testcontainers 适用于此规模 |
large | 多机 | 允许外部网络 | 允许使用持久文件系统 | < 15min | 完整 e2e |
enormous | 分布式 | 广域网络 | 任意位置 | 更长 | 集群 / 混沌测试 |
测试的类型(unit/integration/e2e)和规模(small/medium/large)彼此独立:小规模集成测试(通过 JDBC 在同一进程中使用 Testcontainers Postgres)是合理的。
Playwright 与 Cypress 对比(UI 端到端测试)
| 维度 | Playwright | Cypress |
|---|---|---|
| 浏览器 | Chromium、Firefox、WebKit | Chromium、Firefox、WebKit(有限支持) |
| 多标签页 / 多源 | 支持 | 有限支持 |
| 并行执行 | 内置分片 | 付费仪表板或外部工具 |
| 网络拦截 | 稳健的路由级拦截 | cy.intercept |
| 默认选择 | 新项目选择 Playwright,除非团队已统一使用 Cypress | 团队已大量投入 Cypress 时选择 Cypress |
用例设计技巧
使用 ISTQB 基础级黑盒技术,推导每种所选测试类型中要测试的内容。参考资料:ISTQB BVA 白皮书、ASTQB 黑盒技术。
1. 等价类划分(EP)
将输入域划分为系统预期表现相同的分区;每个分区只需一个测试。
示例 — discount(orderTotal: number) -> number:
| 分区 | 范围 | 代表性测试输入 | 预期结果 |
|---|---|---|---|
| 低于阈值 | 0 <= total < 100 | 50 | 0% discount |
| 中间档 | 100 <= total < 500 | 250 | 5% discount |
| 最高档 | total >= 500 | 1000 | 10% discount |
| 无效(负数) | total < 0 | -1 | throw / error |
四个测试即可覆盖所有分区。单独使用 EP 会遗漏边界,应与 BVA 结合使用。
2. 边界值分析(BVA)
缺陷常集中在边界处。对于每个边界值 B,测试 B-1、B、B+1(浮点数则测试最小可表示步长)。
示例 — 同一个 discount 函数,边界为 100:
| 测试输入 | 原因 | 预期结果 |
|---|---|---|
99(= B-1) | “低于阈值”分区中的最后一个值 | 0% discount |
100(= B) | “中间档”分区中的第一个值 | 5% discount |
101(= B+1) | 确认不存在偏差两位的问题 | 5% discount |
对边界 500 重复测试:测试 499、500、501。总计:6 个边界测试 + 4 个 EP 测试 = 10 个用例。
B-1 / B / B+1 这组三元组在各个边界处结构相同(输入变化、预期输出变化、断言相同);这很适合采用表驱动测试(见下文第 5 小节)。
3. 决策表
当输出取决于条件组合时使用。每一列代表一条规则。
示例 — canCheckout(cartHasItems, paymentValid, addressOnFile):
| 条件 / 规则 | R1 | R2 | R3 | R4 |
|---|---|---|---|---|
| cartHasItems | T | T | T | F |
| paymentValid | T | T | F | * |
| addressOnFile | T | F | * | * |
| 结果 | 允许 | 拦截:地址 | 拦截:支付 | 拦截:购物车 |
四个测试,每条规则一个(* = 无关条件,通过合并规则省略)。
4. 状态迁移
当行为取决于历史状态时使用。识别状态、事件和禁止的迁移。
示例 — 具有 {draft, submitted, paid, shipped, cancelled} 状态的订单状态机:
| 起始状态 | 事件 | 目标状态 | 测试 |
|---|---|---|---|
| draft | submit | submitted | 正常流程 |
| submitted | pay | paid | 正常流程 |
| paid | ship | shipped | 正常流程 |
| draft | cancel | cancelled | 提前取消 |
| paid | cancel | reject | 禁止 — 需要退款流程,不得直接取消 |
| shipped | submit | reject | 禁止 |
每个合法迁移覆盖一个测试,每个禁止的迁移也覆盖一个测试(负向路径)。
5. 表驱动测试
当 EP、BVA 或决策表分析得出 3 个或更多形状相同的用例(相同的设置、相同的断言,只有输入和预期输出不同——例如解析有效/无效日期格式、按税率档位计算税额、路由规则)时,将它们合并为一个表驱动测试。这些用例成为数据表中的各行;测试主体遍历各行,并逐行执行一次断言。参考资料:Dave Cheney,优先使用表驱动测试;Go wiki:TableDrivenTests。
不要在各用例的设置、框架调用或断言形式有明显差异时强行使用表格。强求统一会用一个名称掩盖真实差异,并产生晦涩的失败消息——这类用例应保留为单独命名的测试。
示例——针对 discount(orderTotal) 的 6 个 EP+BVA 用例(边界值为 100),合并为一个表驱动单元测试(TS / vitest 语法;同一模式也适用于 Go t.Run、JUnit @ParameterizedTest、pytest parametrize):
describe("discount", () => {
const cases: Array<{ name: string; input: number; expected: number }> = [
{ name: "EP: below threshold (typical)", input: 50, expected: 0 },
{ name: "BVA: B-1 at boundary 100", input: 99, expected: 0 },
{ name: "BVA: B at boundary 100", input: 100, expected: 0.05 },
{ name: "BVA: B+1 at boundary 100", input: 101, expected: 0.05 },
{ name: "EP: mid tier (typical)", input: 250, expected: 0.05 },
{ name: "EP: top tier (typical)", input: 1000, expected: 0.10 },
];
for (const c of cases) {
it(c.name, () => {
expect(discount(c.input)).toBe(c.expected);
});
}
});
name 列是必需的:每一行都必须对应一个可单独定位的测试,这样失败时才能指向具体用例,而不是“第 3 行,共 6 行”。需要采用不同断言的行(例如,负数输入用例会抛出异常)应作为独立测试保留在表格之外。
依赖项决策
对于 Gate 2(集成)和 Gate 3(组件/E2E),应审慎选择依赖项。目标是在 CI 中仍能确定性运行的前提下,尽可能贴近真实环境。
| 依赖项类型 | 适用场景 | 不适用场景 | 备注 |
|---|---|---|---|
| 通过 Testcontainers 使用真实基础设施 | DB/Redis/Kafka/Browser,开发需要真实驱动行为,需要隔离的 CI 环境 | 冷启动预算 < 1s,没有 Docker | Postgres / Redis / Kafka / Localstack 集成测试的默认选择 |
| 内存中的伪实现 | 自有接口,语义简单(键值对、列表),测试速度至关重要 | 伪实现与真实实现不一致——在集成边界上引入隐蔽错误 | 适用于六边形架构中的仓储端口,前提是该端口有针对真实基础设施的契约测试 |
| Mock(测试替身) | 单个协作者具有纯接口;测试关注协议(是否以 Y 调用了 X) | Mock 的协作者超过 2 个,或对数据结构使用 Mock(反模式:不完整的 Mock) | Mock 是用于隔离的工具,不是测试对象 |
| HTTP Stub | 调用外部 SaaS,且没有 Testcontainers / Localstack 方案 | 需要 Pact / CDC 时(改用契约测试) | nock(Node)、responses(Python)、WireMock(JVM) |
| 真实外部服务 | 仅用于预发布环境中的冒烟测试 | 单元测试/集成测试/CI——始终具有非确定性 | 仅保留给针对预发布环境的冒烟测试 |
权衡总结:Testcontainers > 内存中的伪实现 > Mock,但成本也按此方向递增。选择成本最低且不会歪曲边界行为的层级。
战略性跳过启发式规则
明确规定哪些情况“不必费心”。跳过这些并非偷懒,而是基于 ISO/IEC/IEEE 29119 风险驱动测试 和 风险驱动测试 的风险调整后投资回报率。
| 跳过项 | 规则 |
|---|---|
| 内部辅助工具不做端到端测试 | 如果产物没有 UI,也没有面向用户的路径,则跳过端到端测试。单元测试 + 集成测试足够。 |
| 部署绑定的消费者 API 不做契约测试 | 如果只有一个客户端使用该 API,且两者一起部署,那么契约测试只会增加维护成本,不会带来解耦收益。 |
| 小型有限域不做基于属性的测试 | 如果输入空间是 enum {A, B, C},等价类划分 + 边界值分析已经覆盖;基于属性的测试只会增加基础设施,不会发现更多错误。 |
| 纯函数不做集成测试 | 为了测试 formatCurrency 辅助函数而添加 Postgres 容器纯属浪费。只做单元测试。 |
| 静态标记不做组件测试 | 如果组件没有状态、没有事件、没有条件渲染,那么快照就足够了,或者可以完全跳过。 |
| 声明式连线不做单元测试 | 对依赖注入绑定、路由注册、架构声明等,应在集成层面断言(例如路由是否调用了正确的处理程序)。 |
| 集成测试可可靠覆盖的内容不做端到端测试 | 根据 Google 端到端测试原则:能用越小的测试覆盖某项行为越好。端到端测试是例外,不是默认选择。 |
| 尖峰验证/临时代码不做测试 | 根据 Beck 的 TDD:如果产物会在几小时内删除,请与人工协作者记录这一例外。然后为保留的版本编写测试。 |
| 不编写包含“和”的测试 | 如果测试名称包含“和”,就将其拆分为多个独立测试(每种行为一个断言)。 |
测试矩阵架构
每项测试策略都必须采用以下 YAML 代码块表示。每个列表项中的字段顺序至关重要——评审者和下游工具会将第一个键解析为关键字段(rationale / reason / why),将第二个键解析为分类字段(type / what)。
架构
test_strategy:
artifact: "<path or short identifier>"
rationale: "Why this test strategy is being applied to this artifact (specific, evidence-based)"
criticality: "NONE | LOW | MEDIUM | MEDIUM-HIGH | HIGH"
selected_types:
- rationale: "Why this type is being applied to this artifact (specific, evidence-based)"
type: "unit | integration | component | e2e | smoke | contract | property-based"
size: "small | medium | large | enormous"
framework: "vitest | jest | pytest | go test | JUnit | playwright | cypress | pact | hypothesis | ..."
dependencies:
- "List of dependencies: real Postgres via Testcontainers, in-memory fake, mocked HTTP via nock, etc."
gate: "Gate N (the gate that triggered this selection)"
rejected_types:
- reason: "Why this type does NOT apply to this artifact (cite Strategic Skip Heuristic or gate that did not trigger)"
type: "unit | integration | component | e2e | smoke | contract | property-based"
deliberately_skipped:
- why: "Cost / risk justification for skipping despite a partial signal"
what: "A specific category of test cases being skipped (e.g., 'browser compatibility on IE11', 'load testing beyond 100 RPS')"
YAML 示例
test_strategy:
artifact: "POST /users (user registration endpoint)"
rationale: "User registration is a critical user-facing path; can be used by web and mobile apps independently of each other."
criticality: "MEDIUM-HIGH"
selected_types:
- rationale: "Endpoint contains validation logic (email format, password rules, uniqueness) — Gate 1 ON for branch coverage"
type: "unit"
size: "small"
framework: "vitest"
dependencies: ["in-memory user repository fake"]
gate: "Gate 1"
- rationale: "Endpoint writes to Postgres and emits user.created event to Kafka — Gate 2 ON, real boundary behavior matters"
type: "integration"
size: "medium"
framework: "vitest + supertest + Testcontainers"
dependencies: ["Postgres 15 via Testcontainers", "Kafka via Testcontainers"]
gate: "Gate 2"
- rationale: "Consumed by mobile app and web app on independent deploy cadences — Gate 4 ON, prevents drift"
type: "contract"
size: "medium"
framework: "Pact"
dependencies: ["Pact broker"]
gate: "Gate 4"
rejected_types:
- reason: "No UI surface in this artifact — Gate 3 OFF"
type: "component"
- reason: "No UI surface — Gate 3 OFF; e2e covered by web/mobile apps separately"
type: "e2e"
- reason: "Input domain (email, password) is large but invariants are well-covered by EP+BVA at unit level — property-based ROI is low at MEDIUM-HIGH criticality, only triggers Gate 6 partially"
type: "property-based"
deliberately_skipped:
- why: "Project does not have post-deploy probe pipeline yet; smoke would be no-op"
what: "Smoke test for /users after deploy"
- why: "Non-functional load testing is out of scope for this task; tracked separately in performance backlog"
what: "Load test verifying p99 < 200ms at 1000 RPS"
字段顺序检查清单(评审者会逐字检查):
test_strategy:artifact在rationale之前,rationale在criticality之前。selected_types[*]:rationale在type之前,type在size之前,size在framework之前,framework在dependencies之前,dependencies在gate之前。rejected_types[*]:reason在type之前。deliberately_skipped[*]:why在what之前。
测试用例列表架构
矩阵之后,使用扁平的 Markdown 项目符号列表列出要实现的测试用例。这与 YAML 矩阵分开,因为:
- a. 它列出要测试的内容,而不是方式
- b. 它关联到验收标准
格式
## Test Cases to Cover
### AC-N: [criterion title]
- [type] description
- [type] description
### AC-N: [criterion title]
- [type] description
- [type] description
其中:
type与矩阵中的selected_types[*].type相匹配description遵循 AAA / Given-When-Then(Dan North BDD) 格式,参见 Bill Wake AAA(2001)AC-N引用此用例验证的验收标准(如果不关联验收标准,例如基础设施冒烟测试,则省略)
示例
## Test Cases to Cover
### AC-1: Discount returns the correct percentage based on the total
- [unit] discount returns 0% when total = 0 [EP partition: below threshold]
- [unit] discount returns 0% when total = 99 [BVA: B-1 at boundary 100]
- [unit] discount returns 5% when total = 100 [BVA: B at boundary 100]
- [unit] discount returns 5% when total = 101 [BVA: B+1 at boundary 100]
### AC-2: Discount fails when total is invalid
- [unit] discount throws when total = -1 [EP partition: invalid]
### AC-3: /orders saves the order to the database
- [integration] POST /orders persists order to Postgres and returns 201 with order id
### AC-4: /orders rejects duplicate idempotency key
- [integration] POST /orders rejects duplicate idempotency key with 409
### AC-5: /orders/:id returns order by id
- [contract] GET /orders/:id returns schema matching mobile-app pact
资料来源与延伸阅读
以下 14 个来源为上述每道关卡和规则提供依据。如有疑问,请查阅对应关卡链接的来源。
- 测试金字塔——Mike Cohn(2009,Succeeding with Agile)和 Ham Vocke,The Practical Test Pyramid,martinfowler.com。
- 测试奖杯——Kent C. Dodds(2018),The Testing Trophy and Testing Classifications 和 Write Tests。
- Google 测试规模——Mike Bland(2011),Small / Medium / Large;Software Engineering at Google Ch.11;Test Sizes (Google Testing Blog)。
- Google Testing on the Toilet——What Makes a Good End-to-End Test、Testing UI Logic - Follow the User、Origins (Mike Bland)。
- ISTQB Foundation Level——黑盒技术:Boundary Value Analysis white paper;ASTQB Black-Box Techniques。
- ISO/IEC/IEEE 29119——基于风险的测试流程标准。Wikipedia 概述。
- Kent Beck——Test Driven Development: By Example(Addison-Wesley,2002)。出版商页面。ISBN 978-0321146533。
- The Pragmatic Programmer (20th Anniversary Edition)——Hunt 和 Thomas(2019)。pragprog.com。
- AAA 模式——Bill Wake(2001),3A — Arrange, Act, Assert。Given-When-Then——Dan North,Introducing BDD。
- 基于属性的测试——Hypothesis: What is property-based testing?;QuickCheck(Haskell)、fast-check(TS)。
- 契约测试 / 消费者驱动契约——Pact docs;Pactflow CDC explainer。
- Testcontainers——testcontainers.com。
- 表驱动测试——Dave Cheney,Prefer table-driven tests;Go wiki: TableDrivenTests。
- 基于风险的测试——Risk Management During Test Planning (softwaretestinghelp.com)。
示例详解
每个示例包含:
- a. 工件和验收标准
- b. 逐关卡演示
- c. 遵循架构的
test_strategyYAML - d.
Test Cases to Cover列表 - e. 对被排除类型的说明
示例 A —— 纯辅助函数:formatCurrency(amount: number, code: string): string
制品
function formatCurrency(amount: number, code: string): string;
// e.g. formatCurrency(1234.5, "USD") -> "$1,234.50"
// formatCurrency(1234.5, "EUR") -> "€1.234,50"
验收标准:
- AC-1:USD 输出使用
$前缀、逗号作为千位分隔符、句点作为小数分隔符,并保留两位小数。 - AC-2:EUR 输出使用
€前缀、句点作为千位分隔符、逗号作为小数分隔符,并保留两位小数。 - AC-3:对于不支持的代码,抛出
Error("Unknown currency code")。 - AC-4:
amount = 0格式化为"$0.00"/"€0,00"。
关键性:LOW(仅用于显示的辅助函数,不涉及资金流动)。
关卡演练
| 关卡 | 决策 | 原因 |
|---|---|---|
| 0 跳过 | OFF | 包含逻辑 |
| 1 单元测试 | ON | 纯逻辑,按货币代码分支——测试金字塔底层 |
| 2 集成测试 | OFF | 无 I/O,无边界——跳过启发式:纯函数无需集成测试 |
| 3 组件/E2E | OFF | 无 UI 界面 |
| 4 契约测试 | OFF | 不是公共 API |
| 5 冒烟测试 | OFF | 不可部署 |
| 6 基于属性的测试 | ON(部分) | 数值输入无界,但存在不变量(通过解析实现往返、金额单调性)——Hypothesis。在关键性达到 MEDIUM-HIGH 时提升优先级;此处关键性为 LOW,因此仅少量应用(1-2 个属性) |
test_strategy YAML
test_strategy:
artifact: "src/util/formatCurrency.ts"
rationale: "Pure helper function used in display only; no money movement here."
criticality: "LOW"
selected_types:
- rationale: "Pure logic with currency-specific branches and number formatting; EP+BVA on amount, decision table on currency code"
type: "unit"
size: "small"
framework: "vitest"
dependencies: []
gate: "Gate 1"
- rationale: "Amount domain is unbounded floats; invariant 'parseCurrency(formatCurrency(x, c)) ~= x' is stable; sparingly applied (1-2 properties) at LOW criticality"
type: "property-based"
size: "small"
framework: "fast-check"
dependencies: []
gate: "Gate 6"
rejected_types:
- reason: "No I/O, no boundary, no collaborators - Gate 2 OFF"
type: "integration"
- reason: "No UI surface - Gate 3 OFF"
type: "component"
- reason: "No UI surface - Gate 3 OFF"
type: "e2e"
- reason: "Internal helper, not consumed across deploys - Gate 4 OFF"
type: "contract"
- reason: "Library helper, no deploy pipeline target - Gate 5 OFF"
type: "smoke"
deliberately_skipped:
- why: "Locale list is finite (USD, EUR); exhaustive enumeration via decision table is sufficient and more maintainable than i18n property tests"
what: "Property-based fuzzing of currency code beyond known list"
需覆盖的测试用例
### AC-1: USD output uses `$` prefix, comma thousands, period decimal, two decimal places.
- [unit] formatCurrency(1234.5, "USD") returns "$1,234.50" [EP: typical USD]
- [unit] formatCurrency(0.01, "USD") returns "$0.01" [BVA: B+1 smallest non-zero]
- [unit] formatCurrency(-0.01, "USD") returns "-$0.01" [BVA: B-1 negative side]
### AC-2: EUR output uses `€` prefix, period thousands, comma decimal, two decimal places.
- [unit] formatCurrency(1234.5, "EUR") returns "€1.234,50" [EP: typical EUR]
- [property-based] for any non-NaN finite x in [-1e9, 1e9] and code in {USD, EUR}: parseCurrency(formatCurrency(x, code)) is within 0.005 of x [round-trip invariant]
### AC-3: Throws `Error("Unknown currency code")` for unsupported codes.
- [unit] formatCurrency(1, "XYZ") throws Error("Unknown currency code") [Decision table: unknown code]
### AC-4: `amount = 0` formats as `"$0.00"` / `"€0,00"`.
- [unit] formatCurrency(0, "USD") returns "$0.00" [BVA: B at amount=0]
- [unit] formatCurrency(0, "EUR") returns "€0,00" [BVA: B at amount=0 for EUR]
为何排除其他测试类型:该辅助函数没有边界(无需集成测试)、没有 UI(无需组件/E2E 测试),且属于内部库式代码(无需契约或冒烟测试);对于 LOW 关键性,增加测试类型的成本远高于收益。
示例 B — 带数据库和多个消费者的 HTTP POST 端点:POST /users
制品
一个用户注册端点,用于:
- 验证请求正文(电子邮件格式、密码复杂度、年龄 >= 13)。
- 在 Postgres 中检查电子邮件是否唯一。
- 插入用户记录(事务性)。
- 向 Kafka 发送
user.created事件。 - 返回
201和{id, email, createdAt}。 - 输入无效时返回
400,电子邮件重复时返回409。
使用方:移动应用(iOS/Android)和 Web 应用,各自按独立节奏部署。
验收标准:
- AC-1:有效请求返回
201并持久化用户。 - AC-2:电子邮件格式无效时返回
400,并提供字段级错误。 - AC-3:密码不符合策略时返回
400。 - AC-4:电子邮件重复时返回
409。 - AC-5:注册成功时恰好发送一个
user.created事件。 - AC-6:响应架构对移动端和 Web 消费者保持稳定。
关键性:MEDIUM-HIGH(身份验证接口、身份领域、多消费者公共 API)。
关卡演练
| 关卡 | 决策 | 原因 |
|---|---|---|
| 0 跳过 | OFF | 包含大量逻辑 |
| 1 单元测试 | ON | 验证器(电子邮件、密码、年龄)属于纯逻辑 — 测试金字塔底层 |
| 2 集成测试 | ON | 跨越边界:HTTP、Postgres、Kafka — 测试奖杯 的 ROI 最佳点 |
| 3 组件/E2E | OFF(此处) | 此制品没有 UI;UI 位于移动端和 Web 仓库中,并在各自仓库内自行测试 |
| 4 契约测试 | ON | 两个不同的消费者(移动端和 Web)按独立节奏部署 — Pact CDC |
| 5 冒烟测试 | ON | 可部署的 HTTP 服务;部署后探测 /users 注册接口很有意义 — Google E2E |
| 6 基于属性的测试 | OFF | 输入域(电子邮件、密码、年龄)受限,且单元测试已通过 EP+BVA 覆盖充分;关键性为 MEDIUM-HIGH,但对于有界输入,关卡 6 设为 OFF — 跳过启发式 |
test_strategy YAML
test_strategy:
artifact: "POST /users (user registration endpoint)"
rationale: "User registration is a critical user-facing path; can be used by web and mobile apps independently of each other."
criticality: "MEDIUM-HIGH"
selected_types:
- rationale: "Validators (email, password, age) are pure logic; EP+BVA on each field; one test per partition"
type: "unit"
size: "small"
framework: "vitest"
dependencies: ["in-memory user repository fake (for service-level unit if needed)"]
gate: "Gate 1"
- rationale: "Endpoint writes to Postgres and emits to Kafka; mocking these distorts transactional and ordering behavior - Testcontainers gives real boundary fidelity"
type: "integration"
size: "medium"
framework: "vitest + supertest + Testcontainers"
dependencies: ["Postgres 15 via Testcontainers", "Kafka via Testcontainers"]
gate: "Gate 2"
- rationale: "Public API consumed by mobile + web on independent deploy cadences; contract testing prevents schema drift breaking either consumer"
type: "contract"
size: "medium"
framework: "Pact (provider verification)"
dependencies: ["Pact broker", "consumer-published pacts from mobile and web"]
gate: "Gate 4"
- rationale: "Deployable HTTP service with a post-deploy pipeline; one minimal smoke verifies /users responds 201 in the deployed environment"
type: "smoke"
size: "large"
framework: "Playwright (1 critical path)"
dependencies: ["deployed environment URL", "test account seeding"]
gate: "Gate 5"
rejected_types:
- reason: "No UI surface in this artifact - Gate 3 OFF; mobile and web repos own their own component tests"
type: "component"
- reason: "No UI surface - Gate 3 OFF; consumer e2e lives in mobile/web repos"
type: "e2e"
- reason: "Input domain is bounded and EP+BVA at unit level covers it; property-based on this glue endpoint adds infra without finding more bugs - Gate 6 OFF"
type: "property-based"
deliberately_skipped:
- why: "Performance/load testing is out of scope here; tracked in dedicated performance backlog"
what: "Load test verifying p99 < 200ms at 1000 RPS"
- why: "Cross-region failover is owned by infrastructure team, not this endpoint"
what: "Multi-region availability test"
需要覆盖的测试用例
### AC-1: Valid request returns `201` and persists user.
- [unit] validateEmail accepts "[email protected]" [EP: well-formed]
- [integration] POST /users with valid body returns 201 and persists row in Postgres
- [smoke] POST /users in deployed environment returns 201 for a synthetic test account
### AC-2: Invalid email format returns `400` with field-level error.
- [unit] validateEmail rejects "alice@" [EP: missing domain]
- [unit] validateEmail rejects "" [BVA: empty boundary]
- [integration] POST /users with invalid email returns 400 and does NOT persist
### AC-3: Password not meeting policy returns `400`.
- [unit] validatePassword rejects 7-char password [BVA: B-1 at min length 8]
- [unit] validatePassword accepts 8-char password meeting policy [BVA: B at min length]
- [unit] validatePassword accepts 9-char password [BVA: B+1]
- [unit] validateAge rejects 12 [BVA: B-1 at boundary 13]
- [unit] validateAge accepts 13 [BVA: B at boundary 13]
### AC-4: Duplicate email returns `409`.
- [integration] POST /users with duplicate email returns 409 and does NOT emit event
### AC-5: Successful registration emits exactly one `user.created` event.
- [integration] POST /users emits exactly one user.created event to Kafka on success
- [integration] POST /users transaction rolls back when Kafka publish fails [State Transition: failure path]
### AC-6: Response schema is stable for mobile + web consumers.
- [contract] Provider satisfies mobile pact: POST /users response shape matches mobile contract
- [contract] Provider satisfies web pact: POST /users response shape matches web contract
拒绝这些类型的原因:没有 UI 界面(组件/E2E 测试属于消费者应用),输入空间有界(基于属性的测试 ROI 较低),并有意跳过范围外的关注点(负载、多区域),同时说明了理由。
示例 C — UI 表单组件:<RegistrationForm />(Web)
产物
一个 React 表单组件:
- 字段:email、password、confirmPassword、age。
- 客户端验证:电子邮件格式、密码长度 >= 8 个字符且包含大小写字母和数字、两次输入的密码一致、年龄 >= 13。
- 提交到
POST /users。 - 显示字段内联错误和提交级错误(网络错误、409 重复)。
- 请求处理中禁用提交按钮;收到响应后重新启用。
- WCAG 2.1 AA:标签与输入框绑定;通过
aria-live播报错误;验证失败时焦点移至第一个错误。
验收标准:
- AC-1:用户可以提交有效表单,并跳转到
/welcome。 - AC-2:无效电子邮件显示内联提示
"Enter a valid email"。 - AC-3:密码不一致时显示内联提示
"Passwords must match"。 - AC-4:请求进行期间,提交按钮处于禁用状态。
- AC-5:服务器返回 409 时,在表单级显示
"This email is already registered"。 - AC-6:表单支持键盘导航;验证失败时焦点移至第一个错误。
- AC-7:所有输入框都有程序化标签;错误通过
aria-live="polite"播报。
关键性:MEDIUM-HIGH(注册是面向用户的关键流程;许多司法辖区对无障碍性有监管要求)。
关卡演练
| 关卡 | 决策 | 原因 |
|---|---|---|
| 0 跳过 | 关闭 | 涉及行为和无障碍逻辑 |
| 1 单元 | 开启 | 验证辅助函数(validateEmail、passwordsMatch、parseAge)属于纯逻辑 |
| 2 集成 | 关闭(此处) | 组件本身不会跨越真实边界;网络请求在 fetch 层进行模拟。网络集成由 POST /users(示例 B)负责 |
| 3 组件/E2E | 开启(组件)+ 开启(注册流程的 e2e) | UI 界面、关键性为 MEDIUM-HIGH、面向用户的关键流程 — 测试金字塔顶层 + 跟随用户 |
| 4 契约 | 关闭 | UI 使用 API;提供方侧的契约测试见示例 B |
| 5 冒烟 | 开启 | Web 应用已部署;对“注册页面可正常渲染并提交”进行冒烟测试很有意义 |
| 6 基于属性 | 关闭 | 表单输入范围有限;EP+BVA 足以覆盖 |
test_strategy YAML
test_strategy:
artifact: "src/components/RegistrationForm.tsx"
rationale: "React form component used in web app; registration is a business-critical user-facing path."
criticality: "MEDIUM-HIGH"
selected_types:
- rationale: "Validation helpers (validateEmail, passwordsMatch, parseAge) are pure logic; EP+BVA per field"
type: "unit"
size: "small"
framework: "vitest"
dependencies: []
gate: "Gate 1"
- rationale: "UI rendering + interaction within a single component; network mocked at fetch level - tests focus on user-facing behavior per Follow the User"
type: "component"
size: "small"
framework: "vitest + React Testing Library"
dependencies: ["happy-dom", "msw (mock service worker) for fetch"]
gate: "Gate 3"
- rationale: "Registration is a critical user-facing path; one e2e covers the full happy path with real backend (Testcontainers-backed)"
type: "e2e"
size: "large"
framework: "Playwright"
dependencies: ["app server running locally", "Postgres via Testcontainers", "Kafka via Testcontainers"]
gate: "Gate 3"
- rationale: "Web app deploys to staging/prod; smoke verifies /register page loads and form submits in deployed env"
type: "smoke"
size: "large"
framework: "Playwright (1 critical path)"
dependencies: ["deployed environment URL", "test account seeding"]
gate: "Gate 5"
rejected_types:
- reason: "Component does not own a real boundary; network integration is owned by POST /users (provider) - Gate 2 OFF for this artifact"
type: "integration"
- reason: "UI consumes the API; provider contract tests live with the provider (POST /users) - Gate 4 OFF for the consumer"
type: "contract"
- reason: "Bounded input space; EP+BVA at unit level is sufficient - Gate 6 OFF"
type: "property-based"
deliberately_skipped:
- why: "Cross-browser e2e on legacy browsers (IE11) is out of support per project browser matrix"
what: "Browser compatibility e2e on IE11 / Edge Legacy"
- why: "Visual regression (pixel diff) is owned by a separate Storybook chromatic pipeline"
what: "Pixel-level visual regression assertions"
要覆盖的测试用例
### AC-1: User can submit a valid form and is navigated to `/welcome`.
- [unit] validateEmail accepts "[email protected]" [EP: well-formed]
- [unit] parseAge rejects 12 [BVA: B-1 at boundary 13]
- [unit] parseAge accepts 13 [BVA: B at boundary 13]
- [e2e] user fills valid form, submits, and lands on /welcome page
- [smoke] /register page loads and form submits in deployed environment
### AC-2: Invalid email shows inline `"Enter a valid email"`.
- [unit] validateEmail rejects "" [BVA: empty boundary]
- [unit] validateEmail rejects "alice@" [EP: missing domain]
- [component] entering invalid email and blurring shows "Enter a valid email" inline
### AC-3: Mismatched passwords show inline `"Passwords must match"`.
- [unit] passwordsMatch returns true when both equal "Abcd1234"
- [unit] passwordsMatch returns false when one is "" [BVA: empty]
- [component] entering mismatched passwords shows "Passwords must match" inline
### AC-4: Submit is disabled while request is in flight.
- [component] submit is disabled when password and confirmPassword differ
- [component] submit click disables button while request is pending [State Transition: idle -> pending]
### AC-5: 409 response from server shows `"This email is already registered"` at form level.
- [component] 409 response shows form-level "This email is already registered"
### AC-6: Form is keyboard navigable; focus moves to first error on validation failure.
- [component] validation failure moves focus to first error field [a11y]
### AC-7: All inputs have programmatic labels; errors are announced via `aria-live="polite"`.
- [component] form renders email, password, confirmPassword, age, submit [happy path render]
- [component] all inputs have programmatic labels and errors live in aria-live="polite" region [a11y]
拒绝这些类型的原因:此产物是 UI 消费方——其真实边界是 API,而该 API 已在示例 B 中作为集成测试(提供方侧)进行测试。对于范围有限的 UI 输入处理,采用基于属性的测试缺乏必要性。跨浏览器旧版兼容性和视觉回归测试不在范围内,并已明确说明跳过原因。
技能自检
在宣布策略完成之前,加载时请验证:
- 已明确评估所有 7 个关卡(开启/关闭 + 原因)。
-
selected_types[*]的顺序为rationale -> type -> size -> framework -> dependencies -> gate。 -
rejected_types[*]的顺序为reason -> type。 -
deliberately_skipped[*]的顺序为why -> what。 - 每个 AC 至少被一个测试用例引用。
- BVA 用例针对每个数值边界列出
B-1、B、B+1。 - 按照 Google 测试规模为测试指定规模(小/中/大)。
- 测试名称中不含“and”(依据跳过启发式规则)。
- 至少应用了一条策略性跳过启发式规则,或明确考虑后基于理由决定不采用。
如果任何检查未通过,请在交付前修订策略。
覆盖率分析
变异测试和其他覆盖率分析方法(在编写测试后用于评估测试套件质量)记录在配套的 test-coverage 技能中。