mcpField
G
gwt-tester

gwt-tester

github.com/hexsis-llc测试与质量1110 次安装2 天前更新
代码仓库

关于 gwt-tester

gwt-tester 是用于按 Given/When/Then 语义组织可读测试的技能,提供语言无关的规则与流程,并包含 JavaScript/TypeScript 和 Dart 测试约定示例。

主要功能

  1. 01按 Given/When/Then 组织测试
  2. 02遵守嵌套深度和代码块范围规则
  3. 03分离准备、行为与断言
  4. 04在 When 块下组织变体覆盖
  5. 05按复用范围放置测试工具
  6. 06依据检查清单审查和重构测试

适用场景

  1. 01将测试重构为 Given/When/Then 场景
  2. 02明确区分准备、行为和断言
  3. 03覆盖同一行为的多种接口或变体
  4. 04确定测试工具和共享值的放置位置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/hexsis-llc/skills --skill gwt-tester

安装后在 Claude Code 中输入/gwt-tester确认已加载。

详细介绍

GWT 测试器

按照以下说明编写简洁易读、遵循 Given/When/Then 语义的测试。

术语对照表

在此技能中统一使用以下通用术语。

通用术语JS/TS 示例Dart 示例
套件块describegroup
用例块it / testtest
一次性设置(整个套件)beforesetUpAll
每次设置(每个用例)beforeEachsetUp
一次性清理(整个套件)aftertearDownAll
每次清理(每个用例)afterEachtearDown

本文中:

  • “套件块”指 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)

规则

  1. 最大嵌套深度为 3 层:suite -> suite -> case。 例外:当验证相同行为在不同接口或变体中的表现时(例如 via RPC 和 via REST),允许使用 suite -> suite -> suite -> case 来覆盖 When 变体。 在此例外情况下,变体位于第 3 层,Then 用例块位于第 4 层。
  2. 绝不要在另一个 Given 块中嵌套 Given 块。
  3. Given 设置钩子应专注于共享上下文和测试数据,而不是被测的主要行为。
  4. 优先使用仅包含断言的用例块。
  5. 尽可能将可执行行为移入专门的 When 块。在该 When 块中使用设置钩子,确保每个用例只执行一次操作。 如果场景中没有可执行的操作,则省略 When 块,直接将 Then 用例置于 Given 块下。 如果 When 块包含多个变体,则将操作设置钩子放在各个变体块中,而不是父级 When 块中。
  6. 确保块标题描述该块作用域中实际发生的行为。如果 <action> 已在另一个块的设置钩子中执行,就不要将用例标题写成 When <action> ...。
  7. 如果 When 只有一个 Then,则使用单个用例标题(When ... then ...);在这种合并形式中,可以将 When 操作放在用例主体中。
  8. 删除不能提供场景上下文的包装套件块。
  9. 编写明确的场景标题:Given <subject> with <condition>,可选的 When <action>,以及 Then <outcome>。
  10. 根据作用域放置测试工具:
    • 如果某个工具只用于一个测试文件,则将其放在该文件底部,以确保测试块是文件中的首段代码。
    • 如果某个工具会在多个测试文件中复用,则按照现有代码库惯例将其移至共享测试工具中。
    • 如果没有共享工具的惯例,则创建合适的 utils 目录,并将共享工具放在那里。
  11. 仅当某个值被使用多次时,才将其提取到命名变量中,并将其放在最近的共享作用域(例如 Given 或 When)中,以便可预测地集中更新测试数据。

编写工作流

  1. 将场景定义为顶层的 Given ... 套件块。
  2. 仅在 Given 中添加用于共享上下文的设置钩子。
  3. 为每条操作路径添加嵌套的 When ... 套件块。 如果没有要执行的操作,则跳过 When,直接在 Given 下放置 Then 用例。
  4. 如果多个 Then 用例共用同一操作,则在 When 块内的设置钩子中执行该操作。 如果该 When 下存在变体,则在每个变体块内运行设置钩子。
  5. 添加仅包含断言的 Then ... 用例块。
  6. 如果某个 When 只有一个 Then,可以选择将其合并为单个用例标题:When ... then ...。
  7. 如果 Given 钩子中的逻辑仅适用于特定操作,则将其移至 When 钩子。
  8. 如果某个 When 需要覆盖不同接口或变体,则仅在该 When 下嵌套变体块(例如 via RPC、via REST)。
  9. 保持嵌套层级浅,并移除多余的包装块。
  10. 运行测试和格式化工具。

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)
    })
  })
})

重构指南

当被要求重构或检查现有测试时,应根据所有规则进行评估,而不仅仅是触发修改的那条规则。

按以下顺序检查:

  1. 将当前结构映射为 Given、When 和 Then 块。
  2. 将针对特定操作的逻辑从 Given 设置钩子移至正确的 When 块。 没有操作的场景应直接放在 Given 下,不要添加空的 When。
  3. 每次移动逻辑后,立即重新评估是否可以合并:
    • 如果 When 块现在只有一个 Then,则合并为 When ... then ...。
    • 如果仍有其他结果,则保留 When 和多个 Then 用例。
  4. 结构修改后,重新检查标题与作用域是否对齐。
  5. 完成前运行完整的审查清单。

反模式

  • 在 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,并将 Then case 直接放在 Given 下?
  • 存在 When 变体时,操作设置钩子是否放在各个变体块中,而不是父级 When 块中?
  • 每次移动逻辑后,是否重新评估了是否合并 When/Then?
  • suite 和 case 标题是否与各块作用域中的实际行为一致?
  • 单个 When + 单个 Then 的 case 是否合并为一个 case 块?
  • 文件专用工具函数是否放在测试文件底部(测试优先)?
  • 共享工具函数是否移到了符合现有代码库模式的共享位置(若不存在此类模式,则新建合适的 utils 目录)?
  • 只有在值被重复使用不止一次时,才是否将其提取为具名变量,并将其放在最近的共享作用域中(例如 Given 或 When)?
  • 嵌套深度是否 <= 3;是否仅对 When 变体使用 suite -> suite -> suite -> case(第 3 层),并将 Then case 块放在第 4 层(例如 via RPC/via REST)?

评论

支持 Markdown