关于 gwt-tester
gwt-tester 是用于按 Given/When/Then 语义组织可读测试的技能,提供语言无关的规则与流程,并包含 JavaScript/TypeScript 和 Dart 测试约定示例。
主要功能
- 01按 Given/When/Then 组织测试
- 02遵守嵌套深度和代码块范围规则
- 03分离准备、行为与断言
- 04在 When 块下组织变体覆盖
- 05按复用范围放置测试工具
- 06依据检查清单审查和重构测试
适用场景
- 01将测试重构为 Given/When/Then 场景
- 02明确区分准备、行为和断言
- 03覆盖同一行为的多种接口或变体
- 04确定测试工具和共享值的放置位置
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ hexsis-llc/ skills --skill gwt-tester安装后在 Claude Code 中输入/gwt-tester确认已加载。
详细介绍
GWT 测试器
按照以下说明编写简洁易读、遵循 Given/When/Then 语义的测试。
术语对照表
在此技能中统一使用以下通用术语。
| 通用术语 | JS/TS 示例 | Dart 示例 |
|---|---|---|
| 套件块 | describe | group |
| 用例块 | it / test | test |
| 一次性设置(整个套件) | before | setUpAll |
| 每次设置(每个用例) | beforeEach | setUp |
| 一次性清理(整个套件) | after | tearDownAll |
| 每次清理(每个用例) | afterEach | tearDown |
本文中:
- “套件块”指
describe(JS/TS)或group(Dart) - “用例块”指
it/test(JS/TS)或test(Dart) - “设置钩子”指
before/beforeEach(JS/TS)或setUpAll/setUp(Dart) - “清理钩子”指
after/afterEach(JS/TS)或tearDownAll/tearDown(Dart)
规则
- 最大嵌套深度为 3 层:
suite -> suite -> case。 例外:当验证相同行为在不同接口或变体中的表现时(例如via RPC和via REST),允许使用suite -> suite -> suite -> case来覆盖When变体。 在此例外情况下,变体位于第 3 层,Then用例块位于第 4 层。 - 绝不要在另一个
Given块中嵌套Given块。 Given设置钩子应专注于共享上下文和测试数据,而不是被测的主要行为。- 优先使用仅包含断言的用例块。
- 尽可能将可执行行为移入专门的
When块。在该When块中使用设置钩子,确保每个用例只执行一次操作。 如果场景中没有可执行的操作,则省略When块,直接将Then用例置于Given块下。 如果When块包含多个变体,则将操作设置钩子放在各个变体块中,而不是父级When块中。 - 确保块标题描述该块作用域中实际发生的行为。如果
<action>已在另一个块的设置钩子中执行,就不要将用例标题写成When <action> ...。 - 如果
When只有一个Then,则使用单个用例标题(When ... then ...);在这种合并形式中,可以将When操作放在用例主体中。 - 删除不能提供场景上下文的包装套件块。
- 编写明确的场景标题:
Given <subject> with <condition>,可选的When <action>,以及Then <outcome>。 - 根据作用域放置测试工具:
- 如果某个工具只用于一个测试文件,则将其放在该文件底部,以确保测试块是文件中的首段代码。
- 如果某个工具会在多个测试文件中复用,则按照现有代码库惯例将其移至共享测试工具中。
- 如果没有共享工具的惯例,则创建合适的
utils目录,并将共享工具放在那里。
- 仅当某个值被使用多次时,才将其提取到命名变量中,并将其放在最近的共享作用域(例如
Given或When)中,以便可预测地集中更新测试数据。
编写工作流
- 将场景定义为顶层的
Given ...套件块。 - 仅在
Given中添加用于共享上下文的设置钩子。 - 为每条操作路径添加嵌套的
When ...套件块。 如果没有要执行的操作,则跳过When,直接在Given下放置Then用例。 - 如果多个
Then用例共用同一操作,则在When块内的设置钩子中执行该操作。 如果该When下存在变体,则在每个变体块内运行设置钩子。 - 添加仅包含断言的
Then ...用例块。 - 如果某个
When只有一个Then,可以选择将其合并为单个用例标题:When ... then ...。 - 如果
Given钩子中的逻辑仅适用于特定操作,则将其移至When钩子。 - 如果某个
When需要覆盖不同接口或变体,则仅在该When下嵌套变体块(例如via RPC、via REST)。 - 保持嵌套层级浅,并移除多余的包装块。
- 运行测试和格式化工具。
When 变体覆盖
仅在同一个 When 操作下,为变体覆盖额外增加一层嵌套。
将操作设置钩子放在每个变体块内,而不是父级 When 块中。
示例 1(在变体中设置):
suite("Given an endpoint that returns a user", () => {
suite("When requesting user 42", () => {
suite("via RPC", () => {
setup_each(() => {
response = requestUserViaRpc(42)
})
case("Then status is successful", () => {
assert(response.status == 200)
})
})
suite("via REST", () => {
setup_each(() => {
response = requestUserViaRest(42)
})
case("Then status is successful", () => {
assert(response.status == 200)
})
})
})
})
示例 2(每个变体有多个 Then 结果):
suite("Given a user endpoint", () => {
suite("When requesting user 42", () => {
suite("via RPC", () => {
setup_each(() => {
response = requestUserViaRpc(42)
})
case("Then status is successful", () => {
assert(response.status == 200)
})
case("Then user payload is present", () => {
assert(response.body.user != null)
})
})
suite("via REST", () => {
setup_each(() => {
response = requestUserViaRest(42)
})
case("Then status is successful", () => {
assert(response.status == 200)
})
case("Then user payload is present", () => {
assert(response.body.user != null)
})
})
})
})
伪代码模式
suite("Given <subject> with <state>", () => {
setup_each(() => {
// arrange shared context
})
suite("When <action>", () => {
setup_each(() => {
// act
})
case("Then <outcome A>", () => {
// assert only
})
case("Then <outcome B>", () => {
// assert only
})
})
})
无操作场景(跳过 When):
suite("Given <subject> with <state>", () => {
setup_each(() => {
// arrange shared context only
})
case("Then <outcome A>", () => {
// assert only
})
})
标题与作用域对齐
使用与行为发生位置相符的标题。
错误示例:
suite("Given a counter at zero", () => {
setup_each(() => {
// Bad: action belongs in a When block, not Given setup.
counter.increment()
})
case("Then value is one", () => {
assert(counter.value == 1)
})
})
正确示例:
suite("Given a counter at zero", () => {
setup_each(() => {
// arrange
})
suite("When incremented", () => {
setup_each(() => {
counter.increment()
})
case("Then value is one", () => {
assert(counter.value == 1)
})
case("Then value is not zero", () => {
assert(counter.value != 0)
})
})
})
调整逻辑位置以保持块简洁
将逻辑移至负责该行为的最窄范围块中。
- 在
Given设置钩子中保留数据创建和共享测试夹具。 - 将
Given设置钩子中针对特定操作的调用移至相应的When块。 - 保持
Then用例块仅包含断言。 - 只有在合并后的单个
When ... then ...用例中,才在用例主体内执行操作。
合并 When/Then
当一个操作只有一个结果断言时,进行合并。
错误示例(未合并):
suite("Given a counter at zero", () => {
setup_each(() => {
// arrange
})
suite("When incremented", () => {
setup_each(() => {
counter.increment()
})
case("Then value is one", () => {
assert(counter.value == 1)
})
})
})
正确示例(已合并):
suite("Given a counter at zero", () => {
setup_each(() => {
// arrange
})
case("When incremented then value is one", () => {
counter.increment()
assert(counter.value == 1)
})
})
工具函数的位置
根据复用范围选择工具函数的位置。
- 将文件专用的工具函数保留在同一个测试文件中,并放在所有测试块之后。
- 将跨文件使用的工具函数移至代码库共用的测试工具目录。
- 如果仓库没有既定的共享测试工具目录,则创建合适的
utils目录来存放共享测试辅助函数。
重复使用的值
仅当同一个值使用多次时,才将重复值提取为命名变量,并放在测试设置附近。
错误示例(未提取变量):
suite("Given a user lookup", () => {
suite("When searching by user id", () => {
case("Then it returns the matching user", () => {
result = findUser("user-42")
assert(result.id == "user-42")
})
case("Then it records an audit message for that user", () => {
assert(formatAuditMessage("user-42") == "audit:user-42")
})
})
})
错误示例(作用域过宽):
suite("Given a user lookup", () => {
userId = "user-42" // Too broad: only shared inside the When block below.
suite("When searching by user id", () => {
case("Then it returns the matching user", () => {
result = findUser(userId)
assert(result.id == userId)
})
case("Then it records an audit message for that user", () => {
assert(formatAuditMessage(userId) == "audit:" + userId)
})
})
})
正确示例:
suite("Given a user lookup", () => {
suite("When searching by user id", () => {
userId = "user-42" // Nearest shared scope.
case("Then it returns the matching user", () => {
result = findUser(userId)
assert(result.id == userId)
})
case("Then it records an audit message for that user", () => {
assert(formatAuditMessage(userId) == "audit:" + userId)
})
})
})
重构指南
当被要求重构或检查现有测试时,应根据所有规则进行评估,而不仅仅是触发修改的那条规则。
按以下顺序检查:
- 将当前结构映射为
Given、When和Then块。 - 将针对特定操作的逻辑从
Given设置钩子移至正确的When块。 没有操作的场景应直接放在Given下,不要添加空的When。 - 每次移动逻辑后,立即重新评估是否可以合并:
- 如果
When块现在只有一个Then,则合并为When ... then ...。 - 如果仍有其他结果,则保留
When和多个Then用例。
- 如果
- 结构修改后,重新检查标题与作用域是否对齐。
- 完成前运行完整的审查清单。
反模式
- 在
Given中嵌套Given。 - 当操作执行可以移到
When块时,仍将其放在Given设置中。 - 当被测系统可以在
When设置钩子中执行时,仍在 case 块中调用它。 - 在同一个 case 块中混合设置、操作和断言。
- 添加没有新上下文的包装 suite 块。
- 没有要执行的操作时仍添加空的
When块。 - 存在
When变体时,即使只有一个Then结果,也将结构合并为When ... then ...。 - 嵌套深度超过
suite -> suite -> case;When变体除外,可使用suite -> suite -> suite -> case。 - 标题声称某个操作在一个块中执行,但它实际上在另一个块中执行。
审查清单
- 是否审查了所有规则,而不只是促使此次重构的那条规则?
- 是否存在嵌套的
Given块? - 每个 case 块是否只包含断言?
Given设置是否只包含共享上下文?- 操作逻辑是否已移到相关的
When块中? - 如果没有要执行的操作,是否跳过了
When,并将Thencase 直接放在Given下? - 存在
When变体时,操作设置钩子是否放在各个变体块中,而不是父级When块中? - 每次移动逻辑后,是否重新评估了是否合并
When/Then? - suite 和 case 标题是否与各块作用域中的实际行为一致?
- 单个
When+ 单个Then的 case 是否合并为一个 case 块? - 文件专用工具函数是否放在测试文件底部(测试优先)?
- 共享工具函数是否移到了符合现有代码库模式的共享位置(若不存在此类模式,则新建合适的
utils目录)? - 只有在值被重复使用不止一次时,才是否将其提取为具名变量,并将其放在最近的共享作用域中(例如
Given或When)? - 嵌套深度是否 <= 3;是否仅对
When变体使用suite -> suite -> suite -> case(第 3 层),并将Thencase 块放在第 4 层(例如via RPC/via REST)?
评论
支持 Markdown