← 全部材料 测试
TESTING · 9 个高频考点

测试要让人敢改代码

测试题最容易被问倒的地方不是不会写,而是不知道该测什么:测实现还是测行为、mock 到什么程度、覆盖率怎么看。这三个问题答错,写再多测试也是负债。

01按成本选层级单元多、组件次、E2E 只留关键路径。别被覆盖率牵着走。
02测行为不测实现问自己"换个写法还过不过",不过就说明测错了东西。
03认清假测试只断言 mock 被调用、忘记 await,这两种通过率 100% 但毫无价值。
1

测试金字塔:为什么单元测试要多★

不是教条,是成本曲线
金字塔不是"单元测试更高级",而是成本和反馈速度的权衡:单元测试毫秒级、失败原因精确;E2E 分钟级、失败原因模糊。所以"该测什么"由"出错的代价"决定,不是由"哪种测试更时髦"决定。
三层各管什么【单元测试】纯函数、工具、算法、状态逻辑 速度:毫秒 失败定位:精确到函数和分支 数量:最多 例:formatMoney、debounce、权限判断函数 【组件/集成测试】组件的渲染和交互 速度:百毫秒 失败定位:到组件级别 数量:中等 例:表单校验、列表渲染、loading 与空态切换 【E2E】关键业务流程 速度:秒到分钟 失败定位:模糊(可能是网络、可能是选择器、可能是真实 bug) 数量:最少,只覆盖"挂了就发不出去"的路径 例:登录、下单、支付
面试怎么答测试金字塔的本质是成本和反馈速度的权衡,不是哪种测试更高级。单元测试跑得快、失败原因精确、成本低,E2E 慢而且脆弱,UI 改个类名就可能挂。所以单元测试最多,E2E 只覆盖登录、下单、支付这类挂了就发不出去的路径。前端更常用的是奖杯模型,组件测试最多,因为前端大量逻辑在组件里。测试的核心价值是让人敢改代码,如果一个测试让你不敢重构,说明它测错了东西。
2

Vitest 与 Jest:现在怎么选

API 几乎一样,差别在生态
两者 API 基本兼容(describe / it / expect 一样),主要差别是:Vitest 原生 ESM + 复用 Vite 配置,启动快;Jest 生态更成熟、更多历史项目在用。新项目用 Vite 就选 Vitest,几乎不用额外配置。
两个关键差别【ESM 支持】 Jest 需要 babel / ts-jest 转换,配置成本高 Vitest 原生跑 ESM,复用 vite.config.ts 里的 alias 和插件 → 改一处配置,构建和测试都生效 【速度】 Vitest 用 Vite 的模块图,只编译被引用的部分 大项目里冷启动差异明显 【生态】 Jest:成熟、插件多、问题搜得到答案 Vitest:成长中,但常用能力(jsdom、覆盖率、mock)都齐了
面试怎么答Vitest 和 Jest 的 API 基本兼容,describe、it、expect 都一样。主要差别是 Vitest 原生支持 ESM 并且复用 Vite 配置,所以别名、插件、环境变量不用重复配,冷启动也更快;Jest 生态更成熟,历史答案多。新项目用 Vite 就选 Vitest 基本零配置。需要注意不要混用,因为 vi 和 jest 两个命名空间会互相污染,迁移主要是把 jest 前缀换成 vi。
3

组件测试:测用户看到什么★

Testing Library 的核心主张
Testing Library 只有一条主张:像用户一样找元素——按文字、按角色、按 label 找,不按 class 和 id 找。这样测试不会因为改样式而挂,而且天然验证了可访问性(如果按角色找不到,说明无障碍做错了)。
选择器优先级(官方推荐顺序)1. getByRole ← 首选,按钮、输入框、标题 2. getByLabelText ← 表单字段 3. getByPlaceholderText 4. getByText ← 普通文本 5. getByDisplayValue 6. getByAltText ← 图片 7. getByTitle 8. getByTestId ← 最后手段,改成纯样式时也不要动它 getBy* 找不到就抛错(同步) queryBy* 找不到返回 null(用于断言"不存在") findBy* 返回 Promise(用于等待异步出现)
面试怎么答组件测试的原则是像用户一样找元素,按角色、label、文字找,不按 class 和 id 找。这样样式重构不会让测试挂,而且按角色找不到就说明无障碍做错了,等于顺便验证了可访问性。选择器优先级是 role 最好,其次 label、text,testid 是最后手段。交互要用 userEvent 而不是 fireEvent,因为它模拟完整的 focus、键盘、点击顺序。另外一定要补 loading、空态、错误这三种状态。
4

mock:什么时候该 mock,什么时候不该★

过度 mock 的测试等于自欺欺人
mock 的判断标准只有一条:这个依赖是否属于"被测单元的外部边界"。网络请求、时间、随机数、第三方 SDK 该 mock;同项目里的其他模块、以及你要验证的行为逻辑,不该 mock——mock 掉就等于把要测的东西换成了假的。
面试怎么答mock 的判断标准是这个依赖是不是被测单元的外部边界。网络请求、时间、随机数、第三方 SDK 应该 mock,因为不 mock 测试会不稳定;但同项目里的业务模块不该 mock,mock 掉就等于把要测的东西换成假的。最常见的假测试是只断言 mock 被调用,却不断言参数和最终结果。网络 mock 更推荐用 MSW,它在网络层拦截,组件代码完全不用改,行为更接近真实。
5

覆盖率:怎么读才不被骗

100% 覆盖率不代表没有 bug
覆盖率衡量的是"代码被执行过",不是"逻辑被验证过"。一个 if 走通了但没有断言,覆盖率照样算。所以覆盖率是发现"哪里没测"的工具,不是"测试好不好"的指标。
◐ 可交互:把断言全关掉,看覆盖率还涨不涨
01formatMoney(1234.5) 返回 ¥1,234.50覆盖 18 行
02空数组时返回 "¥0.00"覆盖 6 行
03非法输入不抛错覆盖 12 行
04大数字用千分位覆盖 9 行
行覆盖率100%
真实断言数4
能抓到的 bug4 类
关掉断言后,覆盖率不会掉 —— 它只统计"代码被执行过"。
四种覆盖率,区别在哪行覆盖 line 每一行执行过就算覆盖 最容易注水:调用一次函数就 100% 分支覆盖 branch if / else 两个方向都走过算覆盖 比行覆盖有意义,但仍不保证断言正确 函数覆盖 function 每个函数被调用过 语句覆盖 statement 类似行覆盖 真正有意义的是"变异测试": 故意把代码改错(> 改成 >=), 看测试会不会失败。不失败说明这个断言是空的。
面试怎么答覆盖率衡量的是代码被执行过,不是逻辑被验证过。一个 if 走通了但没写断言,覆盖率照样算。所以它的价值是发现哪里没测,不是评价测试质量。最容易注水的写法是只断言不抛错。实际使用上,打开报告看未覆盖的分支更有用,那些通常正好是错误处理和边界情况;新项目 70-80% 是合理目标,核心逻辑可以高一些,UI 组件不必强求,强制 100% 的结果往往是写一堆空断言。
6

E2E:Playwright 与 Cypress

只测关键路径
E2E 的价值在于验证整条链路能走通,不在于覆盖面。所以只写"挂了就发不出去"的路径:登录、核心下单、支付、关键表单提交。工具上 Playwright 现在更主流(多浏览器、多标签页、无 iframe 限制),Cypress 开发体验好但限制多。
选型差别Playwright ✅ Chromium / Firefox / WebKit 三套引擎 ✅ 多标签页、多窗口、iframe 都支持 ✅ 自动等待(元素可交互才操作),不需要手写 waitFor ✅ 可以直接调 API 做数据准备(request fixture) ❌ 生态相对新 Cypress ✅ 开发时的时间旅行调试体验很好 ✅ 上手简单 ❌ 单标签页限制(跨域跳转要特殊处理) ❌ iframe 支持差 ❌ 同一时刻只能跑一个浏览器
面试怎么答E2E 的价值是验证整条链路能走通,所以要克制,只测登录、核心下单、支付、关键表单这类挂了就发不出去的路径,数量控制在十几条以内。工具上 Playwright 现在更主流,支持三套浏览器引擎、多标签页、iframe,还有自动等待;Cypress 调试体验好但有单标签页和 iframe 的限制。E2E 最大的敌人是不稳定,所以不要用固定 sleep,要等元素或等网络响应,并且测试数据要自己造自己清。
7

测行为还是测实现★

这条判断决定了测试值不值钱
一句话原则:测"输入什么、输出什么",不测"中间怎么做的"。测实现的测试在重构时必然失败,而它失败不代表代码错了——这种测试就是负债。

测实现(会变成负债)

  • 断言组件内部 state 的值
  • 断言某个私有方法被调用了
  • 断言具体的 DOM 结构层级
  • 断言调用了哪几个 API、按什么顺序
  • 断言 class 名

测行为(重构友好)

  • 断言渲染出来的内容
  • 断言用户操作后的可见结果
  • 断言最终发出的请求体
  • 断言页面跳转到了哪里
  • 断言错误提示出现了
同一个需求,两种写法需求:输入金额,超过 1000 打九折 【测实现】❌ it('调用了 calcDiscount', () => { render(<Price />); input('1200'); expect(spyCalc).toHaveBeenCalledWith(1200); }); → 以后把打折逻辑内联进来,这个测试就挂了, 但功能完全正常。 【测行为】✅ it('超过 1000 显示折后价', () => { render(<Price />); input('1200'); expect(screen.getByText('1080')).toBeInTheDocument(); }); → 不管内部怎么重构,只要结果对就通过。
面试怎么答原则是测输入和输出,不测中间怎么做的。测实现的测试在重构时必然失败,而失败不代表代码错,这种测试就是负债。判断方法是问自己"如果换一种完全不同的写法实现同样功能,这个测试会不会挂",会挂就说明测了实现。值得测的内部逻辑应该抽成纯函数单独测,这反过来也会推动代码结构变好。
8

异步测试与假时钟

为什么 await 写错了测试就假通过
异步测试的两个陷阱:忘记 await 导致断言没执行、用固定 sleep 导致测试又慢又脆。正确做法是用 findBy* 等元素出现,或者用假时钟精确推进时间。
面试怎么答异步测试最常见的错误是忘记 await,测试会直接通过但断言根本没执行,这是最隐蔽的假通过,所以要开 no-floating-promises 这条 lint 规则。第二是不要用固定 sleep,要等具体条件,断言"稍后出现"用 findBy,断言"不存在"用 queryBy。测定时逻辑用假时钟手动推进时间,但要注意假时钟和基于真实时间的轮询会打架,得用 advanceTimersByTime 配合 waitFor。
9

CI 里怎么跑

让测试真正拦住问题
测试写好了不代表有用,必须卡在合并前。最小可用方案是:PR 时跑单元和组件测试,主分支合并后跑 E2E,并且红灯必须真的拦住合并——一旦允许"先合再说",测试就死了。
一条合理的分层流水线【PR 触发】 lint + type check 30 秒 单元 + 组件测试(只跑受影响的) 1-2 分钟 → 任一失败,禁止合并 【合并到主分支】 全量单元测试 构建 + 部署到预发 E2E 冒烟(十几条关键路径) → 失败自动回滚 【定时(每天)】 全量 E2E 依赖漏洞扫描 性能基线对比
面试怎么答测试写在 CI 里才有价值,关键是红灯必须真的拦住合并,一旦允许先合再说测试就等于没有。合理分层是 PR 时跑 lint、类型检查和单元组件测试,几分钟内出结果;合并后跑构建加 E2E 冒烟;每天定时跑全量和依赖扫描。大仓库要用增量测试和并行分片控制时间,失败时必须产出截图、trace、日志。另外不稳定测试要立刻修或删,否则团队会习惯重跑,真 bug 也会被重跑掉。
10

速答清单

考前十分钟只看这一页
01测试金字塔为什么单元测试多?
成本和反馈速度的权衡。单元测试毫秒级、失败定位精确;E2E 分钟级、失败原因模糊还可能是不稳定。跟"哪种更高级"无关。
02前端更常用什么模型?
奖杯模型:组件测试最多,单元次之,E2E 最少。因为前端大量逻辑在组件里,纯函数不多。
03测试的核心价值是什么?
让人敢改代码。如果一个测试让你不敢重构,说明它测错了东西,是负债。
04Vitest 和 Jest 怎么选?
新项目用 Vite 就选 Vitest,原生 ESM 且复用 Vite 配置,零额外配置;老项目或需要成熟生态用 Jest。不要混用。
05组件测试按什么找元素?
按 role 最好,其次 label、text,testid 是最后手段。按 class 找的测试会在样式重构时失效,等于没测。
06userEvent 和 fireEvent 的区别?
userEvent 模拟完整交互(focus、键盘、点击顺序),fireEvent 只派发单个事件。很多 bug 只在完整交互下出现。
07组件测试必须补哪些状态?
loading、空数据、错误。只测成功路径的测试,上线后最容易在这三处翻车。
08什么时候该 mock?
依赖属于被测单元的外部边界时:网络、时间、随机数、第三方 SDK。同项目的业务模块不该 mock。
09什么叫假测试?
只断言 mock 被调用,却不断言参数和最终结果。通过但什么都没验证。
10网络 mock 用什么更好?
MSW。它在网络层拦截,组件代码不用改,行为更接近真实环境,比 mock 掉整个 api 模块好。
11覆盖率能说明什么?
只能说明代码被执行过,不说明逻辑被验证过。if 走通了没写断言也算覆盖。它的价值是发现哪里没测。
12覆盖率怎么防注水?
看未覆盖的分支而不是百分比,那些通常是错误处理和边界。或者要求增量代码的覆盖率,比整体百分比更有约束力。
13怎么判断测试测了实现?
问"换一种完全不同的写法实现同样功能,这个测试会不会挂"。会挂就是测了实现。
14异步测试最容易犯什么错?
忘记 await,测试直接通过但断言没执行。要开 no-floating-promises 规则。
15测定时逻辑怎么做?
用假时钟手动 advanceTimersByTime,不要真等。注意假时钟和基于真实时间的轮询会打架。
16E2E 为什么要克制?
慢、脆、贵。挂十次八次是环境问题时团队就会忽略红灯,那测试就白写了。只测挂了就发不出去的路径。
17CI 里测试怎么才有用?
红灯必须真的拦住合并。一旦允许"先合再说",测试就形同虚设。失败必须有截图、trace、日志等产物。