Communitygithub.com

lguidolin/tests-as-a-control

用于编写或修改测试时,当重构导致测试失败,或测试权限/角色规则时。迹象——倾向于编辑测试使其通过,仅测试快乐路径,原本应拒绝的测试突然通过,测试不稳定,或不确定要断言什么。

tests-as-a-control 是什么?

这个技能将测试视为一种“对照样本”,其核心纪律是:不要修改测试去匹配代码,而要理解它们为何不一致。它明确区分了行为变更和结构性变更:行为变更必须遵循“测试先行”原则,即先编写会失败的测试并观察其因正确原因失败,再编写最小代码使其通过;而结构性变更(如重命名、移动、拆分等)则应通过“现有整套测试原封不动地通过”来验证。技能还特别强调公共名称重命名属于行为变更,需要采用“添加并弃用”策略,因为名称本身就是合同。此外,该技能包含了对称覆盖、权限角色测试矩阵、测试按规模分层、不稳定测试立即隔离等严格规则,并列举了常见的错误合理化借口和需要立即停止的红旗场景。该技能适用于Claude Code等AI代理平台,帮助开发者维护测试的完整性和可靠性。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/lguidolin/agent-skills/tree/main/skills/tests-as-a-control

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

测试即对照样本

概述

测试就是一个对照样本:它固定了一个预期,使得任何行为变化都变得可见。纪律是不要修改测试去匹配代码;要理解它们为何不一致。

规则

  • 行为变更:测试先行。 编写会失败的测试,观察它因正确的原因失败,然后编写能通过的最少量代码。如果你没有亲眼看到它失败,你就不知道它测试的是你认为的东西。这个循环本身属于 superpowers:test-driven-development;本技能规定了测试必须包含什么,以及一旦测试存在,失败意味着什么。无论是否安装了该技能,此规则均适用。

  • 结构性变更由未修改的整套测试来验证。 纯粹的结构性变更——重命名、移动、拆分、提取,且无观察到的行为变化——不需要新的测试。其门槛恰恰相反:整套现有测试无需编辑任何测试即可通过。 这就是为什么测试先行在这里不适用——因为没有需要指定的行为,所以没有测试能先有意义地失败。 如果一项结构性变更迫使你修改测试,那它就并非结构性变更。停下来,用下一条规则重新分类。

  • 跨越合同边界的重命名不是重构。 关键问题不在于“代码是否改变了行为?”,而在于“这个名称是否对你无法控制的事物可见?”

    重命名对象分类门槛
    内部符号、私有辅助函数、本地文件结构性现有整套测试无需修改即可通过
    导出函数、API/GraphQL 字段、路由、CLI 标志、数据库列、事件名称、配置键行为性——它破坏了调用方测试先行 + 添加并弃用

    即使对同一数据进行相同操作,公开重命名仍然是破坏性变更,因为名称本身就是合同(海勒姆定律——参见 performance-and-scale)。将其视为行为性变更:通过测试先行周期添加新名称,在弃用窗口期保留旧名称及其通过的测试,只有在调用方迁移完成后才移除旧名称。

    第三种情况是测试以字符串形式引用名称——快照、DI 容器键、反射、fixture 路径。这些在内部重命名时会失败。这不是规则失效;而是表明测试与实现细节耦合了。修复耦合,而不是重新基线化。

  • 重构不得修改测试;需求变更才能修改。 不同的行为:

    • 重构导致测试失败 → 测试捕获了非预期的行为变化。停下来理解它。 编辑测试以使重构通过,会破坏你的仪器。
    • 需求确实发生了变化 → 有意识地更新测试,并作为独立可审查的变更记录下来。
    • 禁止的行为是静默地重新基线化。
  • 对称覆盖——每个规则都要双向测试。 证明授权执行者能够 并且 未授权执行者不能。仅测试快乐路径只能测试规则的一半。

  • 权限/角色规则是一个矩阵: 每个有意义的(角色 × 动作 × 资源)单元格,都需测试允许和拒绝。新角色只有在其拒绝案例存在时才算完整。一个原本拒绝的测试突然通过了,这是安全回归——必须显式审查并接受,绝不能重新基线化。

  • 入口点不得静默添加/移除。 路由、命令、端点各自都需要存在性及冒烟测试覆盖,以便添加/移除操作强制触发测试修改。

  • 按规模测试: 小型(纯逻辑,无 I/O,常量),中型(真实数据库/进程,隔离),大型(全栈)。小型测试多,中型测试少,大型测试最少。警惕缺失的中间层——既非单元测试也非集成测试覆盖的服务器/应用逻辑,因为每一层都假设另一层会测试它。

  • 不稳定测试一经发现立即隔离。 一个在未改代码的情况下随机通/不通过的测试,会破坏对整个测试套件的信任。立即隔离(移出阻塞门,提交修复方案)——永远不要容忍间歇性失败,也不要静默删除。小型测试必须是确定性的:没有真实时钟、随机性或网络。

  • 测试代码要 DAMP,不要 DRY。 描述性且有意义的短语优先于去重。测试必须在隔离阅读时一目了然。对测试应用 DRY 是违规,而非美德(参见 change-hygiene-and-code-craft)。

常见的合理化借口

借口现实
“我先写代码,再补测试”那么测试就是为代码定制的,而不是为需求定制的。你从未看到它失败,所以你不知道它测试了什么东西。
“只是重命名,不需要测试”正确——如果名称是内部的。如果是导出/公开的,名称就是合同,重命名属于行为性变更。
“我重命名了它,不得不修复三个测试,这很正常”不正常。要么你跨越了合同边界,要么那些测试与实现细节耦合了。重新分类。
“测试现在不对了,我就更新一下”仅当需求改变时才这样做。如果你是在重构,测试捕获了真实的变化——调查它。
“我测试了快乐路径,这就够了”一个规则直到拒绝案例也被测试才算完整。
“这个拒绝测试现在通过了,太好了,发布吧”一个被禁止的动作成功了是安全回归,不是进展。
“测试不稳定,重新运行 CI 就行”不稳定性掩盖了真正的 bug,并训练所有人忽视失败。隔离并修复。
“让我用共享辅助函数把这些测试 DRY 一下”测试要 DAMP。有助于现场清晰性的重复应当保留。

红旗——立即停止

  • 在编写会失败的测试之前,为行为变更编写生产代码
  • 结构性变更的 diff 也触及了测试文件
  • 在原地重命名公开/导出的名称,而不是采用添加并弃用策略
  • 在重构期间编辑测试以使其通过
  • 新的权限没有拒绝案例的测试
  • 在未理解变更原因的情况下,重新基线化快照/断言
  • 包含真实的 sleep、Date.now()、随机数据或网络调用的测试
  • 以牺牲可读性为代价,为去除重复而重构测试代码

完整理由:宪法第四条,捆绑于 engineering-constitution/references/engineering-constitution.md。关于 GraphQL/路由合同的具体细节,请参见 graphql-contract-testing。

Individual skills in this repo

This repo contains 18 individual skills — each has its own dedicated page.

lguidolin/change-hygiene-and-code-craft

Use when writing or refactoring code, structuring a commit or PR, or deciding whether to abstract duplication. Symptoms — mixing reorg with logic changes, a PR doing several things at once, a file growing large, the second copy of similar code, or unsure whether to DRY something up.

lguidolin/cloud-delivery-aks

Use when deploying to Kubernetes or Azure Kubernetes Service (AKS), configuring cloud secrets, setting up progressive rollout/canary, per-PR ephemeral environments, or k8s health probes. Keywords — Kubernetes, AKS, Key Vault, Argo Rollouts, Flagger, canary, blue-green, liveness, readiness, PodDisruptionBudget, HPA, rollback, GHCR.

lguidolin/commit-history-rewrite

Use when an existing repository has messy commit history that needs to conform to conventional commits before adopting release-please, or when intermediate WIP/fixup/merge commits need to be cleaned up.

lguidolin/conventional-commits-and-releases

Use when committing, writing a commit message, opening a PR that will be squash-merged, or configuring automated versioning/changelogs. Keywords — conventional commits, release-please, semver, feat/fix/chore, breaking change, changelog.

lguidolin/defense-in-depth-security

Use when handling untrusted input, secrets, authentication/authorization, or dependencies — or threat-modeling a new surface. Keywords — STRIDE, threat model, least privilege, secrets management, supply chain, dependency scanning, input validation, audit log, defense in depth.

lguidolin/designing-before-building

Use when starting a feature, fixing a non-trivial bug, or about to write implementation code — before any code exists. Symptoms you need this: "this is simple, I'll just code it", reaching for the editor before a design is approved, or an idea that hasn't been turned into a spec and plan.

lguidolin/engineering-constitution

Use when starting work in a project that follows the engineering constitution, orienting to its rules, or deciding which engineering practice applies to a task — spec writing, commits, testing, security, deploys, database, or UI work.

lguidolin/graphql-contract-testing

Use when writing a GraphQL query/mutation that the UI and a test will share, or building route/schema contract or smoke tests. Symptoms — copying a query into a test, a test asserting on query text, schema change that didn't break the UI build, or RLS/permission drift. Keywords — graphql-codegen, typed document, contract test, route smoke test.

lguidolin/init-repo-CI

Use when setting up a new repository with conventional commits, release-please, and CI automation, or when retrofitting an existing repository that lacks automated versioning and PR validation workflows.

lguidolin/interface-craft-and-accessibility

Use when building or styling UI — components, layouts, forms, design tokens — or making accessibility decisions. Keywords — a11y, WCAG, keyboard navigation, focus state, contrast, design system, minimalist UI, component reuse, ARIA, semantic HTML.

lguidolin/merge-gates-and-automation

Use when setting up or changing CI, pre-push hooks, or a task runner, or deciding what must pass before merge. Symptoms — tempted to put authoritative checks only in a local hook, skip CI, bypass with --no-verify, or unsure what gates a merge vs. runs locally.

lguidolin/observability-and-slos

Use when adding logging, metrics, tracing, health checks, SLOs, or alerting — or when building a service surface that needs to be operable and debuggable. Keywords — structured logs, OpenTelemetry, correlation id, RED metrics, liveness, readiness, SLI, SLO, error budget, alerting.

lguidolin/performance-and-scale

Use when working on hot paths, list endpoints, pagination, data-access in loops, or public interfaces/schemas. Symptoms — unbounded queries, N+1 access, no latency budget, optimizing without measuring, or changing an interface many consumers depend on. Keywords — pagination, N+1, Hyrum's Law, performance budget, bundle size.

lguidolin/postgres-postgraphile-rls-and-sql

Use when writing PostgreSQL, PostGraphile config, Row-Level Security policies, SQL schema files, or working on the Browser→App→PostGraphile→Postgres data path. Keywords — RLS, SECURITY DEFINER, search_path, pgSettings, grants, roles, GraphQL depth limit, query cost, statement_timeout, SQL file organization.

lguidolin/recording-decisions

Use when a design or architecture decision has been made and needs to be captured — writing a decision record or ADR, updating a decision index, noting a deferred idea, or superseding a past decision. Keywords — ADR, decision record, rationale, rejected alternatives, dependency index.

lguidolin/resilience-and-deploy-safety

Use when planning a deploy, designing a rollback, or responding to an incident or writing a postmortem. Keywords — deploy safety, rollback, immutable artifact, progressive delivery, canary, blast radius, incident response, blameless postmortem, error budget.

lguidolin/ship-it

Use when the user wants to ship work — push, PR, archive decision records, merge, and clean up. Handles the full lifecycle from committing final changes through post-merge cleanup including converting specs/plans to compact decision records.

lguidolin/zero-downtime-migrations

Use when changing a database schema where data must survive the change — adding/removing/renaming columns, constraints, indexes, or backfilling. Symptoms — a destructive migration bundled with a code deploy, a NOT NULL column with a backfill, a table-locking UPDATE, or a rename. Keywords — expand/contract, parallel change, backfill, NOT VALID, CREATE INDEX CONCURRENTLY, graphile-migrate.

相关技能