首页
/ Ant Design 测试用例审查方法:识别“用 A 证明 A”的低价值测试

Ant Design 测试用例审查方法:识别“用 A 证明 A”的低价值测试

2026-09-06 18:04:55作者:吴年前Myrtle

在 ant-design(Ant Design)仓库中维护数千个组件测试时,一个核心问题不是“测试能不能跑过”,而是“测试值不值得保留”。本文基于仓库中的测试审查技能定义(.agents/skills/test-review/SKILL.md),系统讲解一套测试用例质量审查方法论:如何用一句话提炼测试声称保护的契约、如何判断断言的 expected 是否来自独立来源、如何拦截样式实现自证与重复覆盖,以及标准的“先结论、后原因”输出格式。读完后,你可以对任意一条 Ant Design 测试用例给出可保留 / 需改写 / 无实际作用的分类结论。

审查边界:只审不写,静态优先

该技能(skill)在 SKILL.md 的 frontmatter 中声明了触发场景:当需要“验证测试 case、review 测试质量、判断测试是否合理、是否‘用 A 证明 A’、是否重复、是否锁定实现细节”时使用。它明确了四条边界约束,这也是整套方法论成立的前提:

  • 只判断,不负责创建或补充测试:不主动新增测试、不主动补回归测试、不主动修改生产代码;
  • 静态审查优先:默认只读代码、diff、文档、demo 和已有测试,不默认运行测试——不把“能不能跑过”当成主要判断依据。只有用户明确要求“跑一下”“验证 red/green”时,才执行测试命令;
  • 不默认执行 npm testnpm run test:update:注意,仓库 package.json 中确实定义了这些脚本,例如 "test:update": "jest --config .jest.js --no-cache -u""test:vitest": "npm run version && vitest run",但审查流程本身不依赖它们;
  • 输出先结论后原因:除非用户追问,否则不展开长篇建议。

一个容易混淆的细节:如果用户明确要求“顺手给改写建议”,可以在结论后补一句改写方向,但主任务仍然是审查,而不是落地实现。

核心判断一:契约是否独立

审查的第一步是提炼契约。用一句话描述这条测试声称在保护什么:

当 <前置条件> 时,<组件/方法> 应该 <可观察结果>。

判断标准很简单:如果这句话只能从当前实现反推出来,这条测试大概率不值得保留。

例如,一条测试断言某个内部函数在特定分支下返回某值,而这个“特定分支 + 返回值”恰好就是生产代码里写死的逻辑,那么这句话只能是读代码后反推出来的——它保护的“契约”其实只是“实现现在长这样”,实现一改测试就红,但它并没有证明任何用户可感知的行为没有退化。

执行流程要求先回答“这条测试到底想保护什么公开行为”,如果回答不稳(说不出一个独立于实现的公开行为),优先判定为:

结论:此用例无实际作用。

核心判断二:expected 必须来自独立来源

断言的期望值(expected)是测试价值的锚点。方法论把来源分成两档:

高质量来源(独立依据):

  • issue / PR 里明确描述的回归现象;
  • 组件文档、API、demo、FAQ;
  • DOM / React / WAI-ARIA / 浏览器语义;
  • 用户可感知的文本、属性、交互、布局结果。

低质量来源(实现自证):

  • 生产代码里的同一个 helper、token、常量、分支逻辑;
  • 在测试里复制一遍实现;
  • “因为实现可能要这样写,所以我断言它这样写了”。

这里的关键区分是:期望值是否与实现同源。如果 expected 是从生产代码里读出来的(哪怕是通过 import 同一个常量算出来的),那测试与实现共享同一个失败模式——实现错了测试可能照旧通过(因为它断言的“正确值”就是从错的实现里来的),实现重构测试就无谓地变红。只有来自 issue 描述、文档承诺、WAI-ARIA 规范这类独立渠道的期望值,才构成真正的回归保护。

核心判断三:优先审查外部行为

方法论给出了明确的断言优先级:

  1. DOM / 文本 / 属性 / role / aria;
  2. callback 的触发与参数;
  3. 用户或使用方能观察到的行为结果;
  4. 只有存在独立契约时,class / style 才能作为代理信号。

如果断言锁定的是具体 CSS 属性、临时 class、内部状态或中间过程,默认先判低价值。

样式实现自证:默认拦截

以下写法默认按“无实际作用”或“需要改写”处理,除非能证明它对应公开契约:

  • toHaveStyle(...)
  • toHaveClass(...)
  • 断言具体 CSS 属性、CSS 变量、临时 class 存在

文档给出的典型反例:

expect(node).toHaveStyle({ whiteSpace: 'nowrap' });

如果它只是验证“实现里加了 nowrap”,而不是验证独立可感知行为,就属于“用 A 证明 A”。

这一点在 ant-design 仓库中有很强的现实背景:仓库 vitest.config.ts 的注释明确指出样式走 CSS-in-JS,测试环境中 css/less 被映射为 identity-obj-proxy(“测试不需要真实样式”),也就是说样式断言验证的是运行时注入的 CSS-in-JS 产物而非最终视觉结果,进一步削弱了 style 断言的契约价值。仓库中确实存在大量使用 toHaveStyle / toHaveClass 的测试(如 components/alert/tests/index.test.tsx 等),从源码结构看,其中相当一部分集中在 semantic.test.tsx 一类文件里断言语义化 className——这类断言之所以可接受,正是因为 antd 的语义化 className 本身是公开 API 文档承诺的“独立契约”,恰好命中第 4 级优先级中“存在独立契约”的例外条件;而没有文档契约背书的临时 class 断言,则应判低价值。

核心判断四:重复覆盖也应判低价值

即使一条测试本身合理,如果契约已被别的测试保护,新增用例也只是负担。以下情况优先判为重复或冗余:

  • 已有 mountTestrtlTest 或其他聚焦行为测试覆盖相同契约;
  • 同一组件已有相同 props 组合和同类断言;
  • 新 case 只是换文案、换变量名、换写法,没有新增行为分支。

仓库中这两类公共 helper 的实现值得对照理解。tests/shared/mountTest.tsx 的全部逻辑只有十几行:

export default function mountTest(Component: React.ComponentType) {
  describe(`mount and unmount`, () => {
    it(`component could be updated and unmounted without errors`, () => {
      const { unmount, rerender } = render(<Component />);
      expect(() => {
        rerender(<Component />);
        unmount();
      }).not.toThrow();
    });
  });
}

它保护的是“组件可被更新和卸载且无报错”这一契约(注释中引用的 PR #18441 就是其独立依据——一个真实的回归现象)。而 tests/shared/rtlTest.tsx 则在 ConfigProvider direction="rtl" 下渲染组件并做快照,保护“RTL 方向下渲染正确”的契约。由于 mountTest / rtlTest 在几乎所有组件测试里被引用(如 components/alert/tests/index.test.tsxcomponents/button/tests/index.test.tsx 等),任何重复手写“组件渲染后容器非空”“挂载卸载不抛错”的独立用例,按方法论都应当判为冗余。

执行流程:五步静态审查

文档给出的完整审查流水线如下,默认不运行任何测试:

1. 静态审查优先

当用户是在验证测试是否合理时:

  • 默认只读代码、diff、文档、demo、已有测试;
  • 默认不运行测试;
  • 不把“能不能跑过”当成主要判断依据。

只有用户明确要求“跑一下”“验证 red/green”时,才执行测试命令(对应 package.json 中的 test:updatetest:vitest 等脚本)。

2. 提炼被保护的契约

回到那句话模板:当 <前置条件> 时,<组件/方法> 应该 <可观察结果>。如果回答不稳,优先判为“此用例无实际作用”。

3. 查独立依据

从以下位置找证据:

  • 当前 PR / issue 描述;
  • 组件文档与 demo(如各组件目录下的 index.zh-CN.mddemo/);
  • 现有测试;
  • 外部语义规范(WAI-ARIA、DOM 语义)。

如果找不到独立依据,而 expected 又来自实现本身,直接判低价值。

4. 查是否重复

需要时用 ripgrep 搜索现有覆盖:

rg -n "<关键字>|<issue号>|<行为描述>" components/<component> tests

如果同一契约已经被保护(典型如 mountTestrtlTest),新 case 通常应判为冗余。

5. 给出分类结论

三档分类标准:

结论 判定条件
此用例可保留 契约独立、断言面向外部行为、不是重复覆盖
此用例需要改写 测试意图可能对,但断言方式锁定实现或证据不足
此用例无实际作用 expected 同源、实现自证、重复覆盖、只测存在性、只测内部细节

输出格式与快速拦截清单

输出格式

默认只输出最终结论,不写调研过程:

结论:此用例无实际作用 / 此用例可保留 / 此用例需要改写
原因:
- ...
- ...

规则:先下结论再给原因;原因保留 2 到 4 条;不要先讲命令、搜索过程、推理链;除非用户追问,否则不展开长篇建议。

快速拦截清单

以下情况默认直接质疑,无需完整走一遍流程:

  • 输入 a,expected 也从同一路径算出 a
  • 断言私有 helper / hook / 中间 state;
  • 断言 classNamewhiteSpacedisplayzIndex 等具体实现;
  • 在已有 mountTestrtlTest 旁边再补同类 case;
  • 只做 toBeTruthy() / toBeDefined() 这类存在性断言。

Ant Design 仓库特有的落地约束

文档最后给出了三条针对 ant-design 仓库的具体约束,使这套通用方法论在仓库内可操作:

  • __tests__ 中引用仓库内代码时使用相对路径:例如 tests/shared/rtlTest.tsximport ConfigProvider from '../../components/config-provider'import { render } from '../utils',即测试代码通过相对路径就近引用被测对象与测试工具;
  • 优先沿用目标组件现有测试结构与 helper:新增审查视角时不引入新的组织方式,而是对照组件已有的测试文件结构判断重复与风格一致性;
  • 对样式问题,先问“能否用更外层行为表达”,再接受 style/class 断言:这与断言优先级中“class / style 只作代理信号”的要求一脉相承。

小结

这套审查方法论的本质,是把“测试有没有价值”从主观感受变成可执行的静态检查:契约能否一句话独立表述、expected 是否来自 issue/文档/规范等独立来源、断言是否停留在外部可观察行为、是否已被公共 helper 或同类用例覆盖。四问皆过才可保留,意图对但断言锁实现的判“需改写”,实现自证与重复覆盖的一律判“无实际作用”。对 ant-design 这样测试文件遍布 components/**/__tests__ 的大型组件库而言,这套流程的价值在于让每一次测试审查都不依赖“跑一遍看结果”,而能在读代码阶段就给出有依据的结论。

登录后查看全文
热门项目推荐
相关项目推荐