想象你是一家餐厅的老板,刚招了一批新厨师。你不可能等客人投诉才回头修菜——那就等于线上事故。于是你定下规矩:每道新菜上市前,必须走完三道尝菜程序。先单独尝每味食材,再尝混合后的酱汁与主料,最后换成顾客身份把整桌菜从头吃一遍。尝菜的过程就是代码里的测试,那张写满尝菜方法、能一键执行的检查清单,就是测试框架。Jest 像你用惯了的标准菜谱,稳妥、全面、资料多;Vitest 则像新开的效率厨房,食材早就在 Vite 传送带上备好,出手快、复用流程顺。本文就用这条后厨主线,带你把测试金字塔、单测写法、Jest 与 Vitest 配置、Mock 替身、覆盖率、快照测试和 TDD 一次讲透——读完你就能在自己的项目里把测试灶台搭起来。

岗位关联

掌握测试框架,等于让代码在交付前先被验证一遍。这是从“能写页面”走向“能扛工程”的分水岭,也是前端基建、CI 质量治理岗位的核心能力。

目录
  1. 可视化演示
  2. 测试金字塔
  3. 单元测试
  4. Jest配置
  5. Vitest优势
  6. Mock模拟
  7. 覆盖率统计
  8. 快照测试
  9. TDD开发
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示测试金字塔层级结构、Jest与Vitest核心能力对比,以及TDD红绿重构循环的开发流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

测试金字塔层级 单元测试:数量最多,快速隔离验证单个函数 E2E测试:数量最少,模拟真实用户操作流程 Unit > Integration > E2E 金字塔结构确保测试速度和覆盖率的平衡

测试金字塔

新菜单上线前,主厨会在后厨把三道尝菜流程走一遍:开火前单独尝每味食材,下锅后尝酱汁与主料的融合度,最后以顾客身份坐在餐桌前完整点一遍。三道程序一道都不能少,但成本差异巨大。测试金字塔,就是把这套尝菜哲学搬到代码世界。

代码里的“味觉关卡”分为三层:单元测试(unit test)锁住单个函数或组件的输出;集成测试(integration test)验证模块与模块的协作;端到端测试(E2E)启动真实浏览器模拟用户完整操作。层级越往上,越接近真实体验,也越慢、越贵、越容易被环境波动影响。

因此工程团队把最多、最快的单元测试放在塔底,少量 E2E 放在塔尖。为什么分层有用?可以用“漏网概率”来理解:假设每层发现缺陷的概率分别是 p_U、p_I、p_E,三层各自独立把关,那么所有层都漏掉才会让缺陷溜到线上。总发现概率是一个简单又经典的概率公式:

主厨不会顿顿把整桌菜从头吃一遍,但会保证每道工序都有人把关。金字塔的比例就是“便宜的多备、昂贵的少用”的平衡——你以后评审测试方案时,第一反应就该是:这层贵不贵?这层够不够多?

P发现=1−(1−pU)(1−pI)(1−pE)\displaystyle P_{\text{发现}} = 1 - (1-p_{\text{U}})(1-p_{\text{I}})(1-p_{\text{E}})
三道关卡各有盲区,但只要任何一道尝出问题,菜就不会上桌;三层全漏才是真正翻车。
  1. p_U / p_I / p_E各层测试单独发现某个缺陷的概率,取值在 0 到 1 之间。
    pU=单元测试抓到的缺陷数缺陷总数p_{\text{U}} = \frac{\text{单元测试抓到的缺陷数}}{\text{缺陷总数}}
  2. 1 减 p这一层漏掉缺陷、让问题继续溜走的概率。
    1−pU1 - p_{\text{U}}
  3. 三层连乘只有三层同时漏掉,缺陷才无人拦截。
    (1−pU)(1−pI)(1−pE)(1-p_{\text{U}})(1-p_{\text{I}})(1-p_{\text{E}})
  4. 用 1 减去全漏从“绝对漏掉”的反面推出“至少一层抓住”。
    1−(1−pU)(1−pI)(1−pE)1 - (1-p_{\text{U}})(1-p_{\text{I}})(1-p_{\text{E}})

单元测试

单元测试相当于开火前单独尝一口盐。你不需要等待整锅菜熟,只需要知道“这一味食材本身有没有坏”。对前端来说,“食材”就是一个个函数、工具库、组件的 Props 计算逻辑。

在 Jest 与 Vitest 中,一个单元测试由三个零件组成:describe 把相关用例归成一个“食材篮子”,it/test 描述具体用例,expect 做出断言。断言是硬比较,不是印象——框架只认“相等、不等、是否为真”这类明确结论。

常见的断言写法有:expect(actual).toBe(expected) 判断引用相等,对象内容比较用 toEqual 做深比较;toBeTruthy、toBeNull、toHaveLength 覆盖常见场景。从抽象角度看,每个用例都是一个纯函数:输入被测函数与参数,输出“过/不过”。

单独尝食材有两大好处:定位快、跑得快。一个工具函数出了问题,describe 里的用例名会直接指出是哪一味食材坏了,而不是让你去翻整本菜谱。

result(f,x)={PASS,f(x)=expectedFAIL,f(x)≠expected\displaystyle \text{result}(f,x) = \begin{cases} \text{PASS}, & f(x) = \text{expected} \\ \text{FAIL}, & f(x) \ne \text{expected} \end{cases}
就像尝一片柠檬时,你的舌头早预期到“酸”;断言就是把这份预期写成白纸黑字。
  1. f(x)被测函数在测试中的真实输出,也叫 actual。
    actual=f(args)\text{actual} = f(\text{args})
  2. expected你写用例时锁定的正确答案。
    expected=你相信的结果\text{expected} = \text{你相信的结果}
  3. 比较命中实际等于期望则通过,否则断言抛异常。
    actual≠expected⇒FAIL\text{actual} \ne \text{expected} \Rightarrow \text{FAIL}
  4. 汇总判定框架收集所有用例结果,决定测试进程退出码,CI 据此拦截。
    exit code={0,全部 PASS1,存在 FAIL\text{exit code} = \begin{cases} 0, & \text{全部 PASS} \\ 1, & \text{存在 FAIL} \end{cases}

Jest配置

Jest 就像后厨那本写满检查清单的标准菜谱:每道工序都规定“切多大、放多少盐”,还带页码快捷键,随时能翻到某一小节执行。它由 Facebook 开源,是目前生态最成熟、文档最全的前端测试框架之一。

Jest 的核心理念是“零配置起步”:默认扫描 *.test.js 与 tests 目录下的文件。你只需 npm install --save-dev jest,再在 package.json 写入 "test": "jest"。需要自定义时,用 npx jest --init 生成 jest.config.js,可配置 testMatch 匹配规则、testEnvironment 指定 JSDOM 或 Node 环境、coverageThreshold 设覆盖率门槛。

常用命令包括:npm test 全量执行,--watch 监听文件变化自动重跑,-t “关键词” 只跑匹配的用例。Jest 还能当质量门禁:在 CI 里,只要行覆盖率或分支覆盖率低于阈值,测试进程就以非零码退出,构建直接红灯。判定逻辑可以用一个条件式表示:

标准菜谱的价值在于可复制:任何新厨师照着清单也能稳定出品。Jest 同理——配置显式、生态成熟,让测试成为团队共识,而不是某个高手的独门手艺。

PASS  ⟺  Cline≥θline∧Cbranch≥θbranch\displaystyle \text{PASS} \iff C_{\text{line}} \ge \theta_{\text{line}} \land C_{\text{branch}} \ge \theta_{\text{branch}}
验收标准写进菜谱:不仅菜要做出来,还得证明每个关键步骤都实操过、且达到规定次数。
  1. C_line当前项目行覆盖率百分比。
    Cline=已执行行数总行数×100%C_{\text{line}} = \frac{\text{已执行行数}}{\text{总行数}} \times 100\%
  2. θ_line配置文件里写死的最低行覆盖阈值。
    θline=80\theta_{\text{line}} = 80
  3. 且(∧)行覆盖与分支覆盖必须同时达标,缺一不可。
    Cline≥θline∧Cbranch≥θbranchC_{\text{line}} \ge \theta_{\text{line}} \land C_{\text{branch}} \ge \theta_{\text{branch}}
  4. 判定输出条件成立则退出码 0、CI 放行;否则红灯拦下合并请求。
    结果={通过,两条件都成立拦截,任一不成立\text{结果} = \begin{cases} \text{通过}, & \text{两条件都成立} \\ \text{拦截}, & \text{任一不成立} \end{cases}

Vitest优势

又一家效率厨房开业了:食材早就在 Vite 传送带上洗净切好(依赖预打包),按下按钮立刻出菜,连隔壁灶台的热处理结果都能复用。Vitest 就是这家效率厨房。

Vitest 是 Vite 生态原生的测试框架,最直接的差异在转译层:Jest 默认还要再配一套 Babel 或 ts-jest 把 TS/JSX 转成 CommonJS,Vitest 直接复用 Vite 的解析与 esbuild 转译,几乎没有额外配置。项目已有 vite.config.ts,Vitest 自动读取,扩展字段写在 test 里。

体验最明显的是启动速度和热更新:大项目里 Vitest 因为共享 Vite 模块缓存,往往比 Jest 快一个量级。它还默认多进程并行跑测试文件,整体耗时近似可以估算:

换灶台不等于废掉菜谱。Vitest 的 API 与 Jest 高度兼容——it、expect、mock 几乎同名,把 import 从 @jest/globals 换成 vitest 基本就是迁移主成本。新项目可以直接上 Vitest,存量 Jest 项目也不必强拆,按需演进。

T=⌈NM⌉×tˉ\displaystyle T = \left\lceil \frac{N}{M} \right\rceil \times \bar{t}
传送带越宽,同时出炉的菜越多;但并行不是无限——机器核数和任务大小决定了上限。
  1. N测试文件总数,也就是要出炉的菜道数。
    N=测试文件数量N = \text{测试文件数量}
  2. MVitest 启动的 worker(并行进程)数量,相当于灶台数。
    M=CPU 核数(可配置)M = \text{CPU 核数(可配置)}
  3. 向上取整 ⌈⌉最后一组不满一车也算一轮。
    ⌈NM⌉\left\lceil \frac{N}{M} \right\rceil
  4. 乘以平均耗时每个文件的平均执行时间,两者相乘得到总耗时估算。
    T=⌈NM⌉×tˉT = \left\lceil\frac{N}{M}\right\rceil \times \bar{t}

Mock模拟

冬天没有鲜蟹,主厨用豆腐调出“赛螃蟹”的味型。这时后厨验证的是刀工与火候,而不是蟹肉供应链。前端测试里的 Mock,就是这道工序里的“豆腐蟹肉”。

前端模块总是依赖 axios、localStorage、setTimeout 这些“外界食材”。真依赖会带来网络抖动、浏览器差异和漫长的等待。Mock 用替身替换它们:vi.fn() 创建替身函数并记录每次调用,vi.mock('axios') 在模块加载层面整体替换导出,使被测模块拿到的永远是可控替身。

替身函数最有价值的能力是“被验证”:你可以断言它被调用过、被调用了 N 次、以特定参数调用过。配合 vi.useFakeTimers() 快进定时器,倒计时、轮询这类用例从等 5 秒变成毫秒级。从数据视角看,mock 函数就是一张调用记录表:

但别把所有食材都换成豆腐——Mock 只模拟外部边界,自己的业务逻辑该真刀真枪。主厨可以用豆腐模拟蟹肉,绝不会用豆腐模拟自家灶台的火候。判断标准就一句话:这层依赖是不是房子外面的墙?

Cmock={(aj,rj)∣j=1,…,k},∀j:aj=exj\displaystyle C_{\text{mock}} = \{ (a_j, r_j) \mid j = 1, \dots, k \}, \quad \forall j: a_j = \text{ex}_j
替身食材回传“已处理好”,后厨才能专心验证自己的翻炒动作对不对。
  1. a_j第 j 次调用替身时传入的实参快照。
    aj=(xj,yj,… )a_j = (x_j, y_j, \dots)
  2. r_j替身提前预设的返回值。
    rj=你指定的模拟结果r_j = \text{你指定的模拟结果}
  3. 调用记录集合 C_mock框架把每一次(参数,返回)存成一份档案。
    Cmock={(a1,r1),(a2,r2),… }C_{\text{mock}} = \{(a_1,r_1), (a_2,r_2), \dots\}
  4. 全称判断 ∀逐一核对每次调用是否符合预期参数。
    ∀j:aj=exj\forall j: a_j = \text{ex}_j

覆盖率统计

收工前主厨会翻台账:菜谱上三百道步骤,今天实际操练了几道?没操练的步骤,明天可能就翻车。测试里的覆盖率,就是这张“实操台账”。

覆盖率指标常见有四类:行(line)、函数(function)、分支(branch)、语句(statement)。行覆盖率统计“这行代码有没有被执行过”;分支覆盖率统计 if/else 的每个真假分支是否都走过。注意:覆盖率只数“尝了几口”,不判断“尝得对不对”——高覆盖不等于高质量。

在 Jest 或 Vitest 里,一条 --coverage 就能生成报告:text 在终端输出表格,lcov 生成 HTML 页面,CI 归档后每个 PR 都能看到增删趋势。coverageThreshold 可以设置全局或按目录的阈值,不达标构建直接失败。核心指标公式:

主厨不会只看台账就开饭,但台账是不可缺的仪表盘。合理策略是:核心业务链路行覆盖 80%、关键分支全部覆盖,把火力集中在风险最高的灶台,而不是无脑追求全项目 100%。

Cline=LcoveredLtotal×100%\displaystyle C_{\text{line}} = \frac{L_{\text{covered}}}{L_{\text{total}}} \times 100\%
台账数的是步骤数量,而不是味道评分;覆盖率问的是“跑没跑过”,不是“断没断言”。
  1. L_covered被插桩探针记录到已执行的行数。
    Lcovered=∣{执行到的行}∣L_{\text{covered}} = |\{ \text{执行到的行} \}|
  2. L_total可执行语句的总行数,注释和空行不算。
    Ltotal=∣{所有可执行行}∣L_{\text{total}} = |\{ \text{所有可执行行} \}|
  3. 比值乘 100%把比例转换成常见的百分比读数。
    LcoveredLtotal×100%\frac{L_{\text{covered}}}{L_{\text{total}}} \times 100\%
  4. 与阈值比较核心目录低于 80% 就红灯,门禁拦住合并。
    Cline≥θlineC_{\text{line}} \ge \theta_{\text{line}}

快照测试

招牌菜上线前,后厨会给它拍一张标准照贴在墙上。下次出菜再拍一张对比:一模一样就放行;有差异必须主厨签字——是改良还是翻车,人说了算。前端快照测试就是这个机制。

在组件测试里,toMatchSnapshot() 第一次运行时会把组件渲染出的 DOM 结构序列化成文本,存进 snapshots 目录;之后每次运行,框架都拿新输出与快照文件逐字符比较。相同则通过,不同则失败并打出 diff。

失败后要人工判断:如果是刻意变更(文案调整、类名重构),确认 diff 内容后按 u 更新快照;如果是意外回归,就得回头修代码。快照适合稳定的 UI 结构、配置文件、接口桩数据;不适合频繁变化的时间戳、随机数、内联样式。差异量可以形式化地表示:

所以快照是“变更提醒器”,不是“正确性保证器”。主厨靠照片发现菜变了,但决定变好变坏的永远是经验主厨本人——快照更新时的 code review,一步都不能省。

D=∑i[ sinew≠siold ],pass  ⟺  D=0\displaystyle D = \sum_{i} [\ s_i^{\text{new}} \ne s_i^{\text{old}}\ ], \quad \text{pass} \iff D = 0
快照对比像检查照片的每一颗像素:差一个像素都不放行,但“为什么差”必须人来回答。
  1. s_i^new本次渲染序列化后的第 i 个字符片段。
    sinew=当前输出的第 i 段s_i^{\text{new}} = \text{当前输出的第 } i \text{ 段}
  2. s_i^old上次保存的快照对应片段。
    siold=快照文件中对应段s_i^{\text{old}} = \text{快照文件中对应段}
  3. 求和统计每个不相等的位置记 1,累计出差异量。
    D=∑i[sinew≠siold]D = \sum_{i} [s_i^{\text{new}} \ne s_i^{\text{old}}]
  4. D=0 才通过任何差异都必须触发 review 流程。
    pass  ⟺  D=0\text{pass} \iff D = 0

TDD开发

顶级后厨研发新菜,会先把顾客口味写成期望卡钉在墙上:“咸度三分、摆盘干净、出菜温度 60 度”,然后才开火。菜还没做出来,验收标准已经在等它了。这就是 TDD 的起点。

TDD(测试驱动开发)的循环有三拍:红灯 -> 绿灯 -> 重构。先用测试描述期望并确认它失败(红),再写最小实现让它通过(绿),最后在测试保护下重构整理内部结构,然后写下一个用例回到红灯。测试不是写码之后的附属品,而是设计工具——它逼你先想清楚输入输出与边界条件。

对前端开发,TDD 尤其适合纯函数、工具库、状态 reducer、解析器这类逻辑密集的代码;UI 视觉层则用组件测试与快照辅助。TDD 跑得勤,重构才有底气下刀,回归成本被大幅压缩。状态流转可以形式化地表示:

新手最容易犯的错,是把 TDD 理解成“先写代码、后补测试”。那就像吃完饭才写期望卡——菜已经端上桌了,晚了一步。真正的 TDD,是让测试先开口说话,代码后接话。

red→最小实现green→重构green→新用例red\displaystyle \text{red} \xrightarrow{\text{最小实现}} \text{green} \xrightarrow{\text{重构}} \text{green} \xrightarrow{\text{新用例}} \text{red}
期望卡在先,炒锅在后;红灯不是坏消息,而是路标。
  1. 红 → 绿写一个最小的实现,让失败测试转绿。
    red→最小实现green\text{red} \xrightarrow{\text{最小实现}} \text{green}
  2. 绿 → 绿在测试全绿的保护下重构代码结构,行为不变。
    green→重构green\text{green} \xrightarrow{\text{重构}} \text{green}
  3. 绿 → 红添加新用例,期望再次落空,开启下一轮。
    green→新用例red\text{green} \xrightarrow{\text{新用例}} \text{red}
  4. 循环不变量每一步结束时测试都必须跑过——红只是过程,绿才是常态。
    每轮结束⇒全部测试通过\text{每轮结束} \Rightarrow \text{全部测试通过}

AI 应用场景

场景 1:组件库上线前的 CI 质量门禁

阶段:持续集成与交付

解决的问题:组件库被多个业务复用,一次回归会炸一片页面。传统方式是上线后等用户反馈,成本极高。用 Jest 给每个组件写单元测试与快照,流水线跑 npm test -- --coverage,行覆盖低于 80% 直接失败;快照 diff 必须维护人确认。就像后厨出菜前按检查清单逐项打勾。

具体案例:某中台团队的 Button 组件在 v2 改版时,快照第一时间发现 DOM 结构变化,评审确认是有意改动后才更新快照并放行,避免了一次波及十几个应用的样式回归事故。

场景 2:业务模块的 Mock 提速验证

阶段:研发效能提升

解决的问题:支付模块依赖外部 SDK 和两次轮询,本地连不上测试环境就没法验证。用 vi.fn 模拟 SDK 返回值、vi.useFakeTimers 快进定时器,几毫秒就能跑完平时要等 5 秒的用例,把“假蟹肉”用在了真正的墙外依赖上。

具体案例:开发在提交前本地跑 200 条单元测试,平均耗时 1.8 秒;提交后 CI 复跑,一次失败用例带着堆栈直接定位到轮询分支,修复只用了一分钟。

场景 3:老项目从 0 到 1 补齐测试

阶段:工程化改造

解决的问题:存量项目一行测试都没有,重构像走钢丝。先给纯函数、状态 reducer 用 TDD 补用例,再快照锁住关键页面结构,最后引入 Vitest 作为统一测试跑手,逐步提高覆盖率阈值,让重构从“赌博”变成“有网的保护”。

具体案例:某遗留管理后台经过三个迭代,测试覆盖率从 5% 提升到 72%,上线发布频次从两周一次提高到隔天一次,线上回归工单减少了六成。

Python 演示

python71 行
# -*- coding: utf-8 -*-"""A tiny Jest-like test runner used in the article demo."""import timeimport traceback  class Suite:    def __init__(self):        self.cases = []        self.results = []     def register(self, name, fn):        self.cases.append((name, fn))     def run(self):        start = time.time()        passed = 0        for name, fn in self.cases:            try:                fn()                self.results.append(("PASS", name, ""))                passed += 1            except AssertionError as exc:                self.results.append(("FAIL", name, str(exc)))            except Exception:                self.results.append(("ERROR", name, traceback.format_exc(limit=1)))        for status, name, detail in self.results:            print("%-5s %-14s %s" % (status, name, detail))        cost = time.time() - start        print("passed %d/%d, cost %.3fs" % (passed, len(self.cases), cost))  def expect(actual):    def to_equal(expected):        assert actual == expected, "expected %r == %r" % (actual, expected)     def to_be(expected):        assert actual is expected, "expected %r is %r" % (actual, expected)     def to_be_true():        assert actual is True, "expected True, got %r" % (actual)     return {"toEqual": to_equal, "toBe": to_be, "toBeTrue": to_be_true}  suite = Suite()  def test_addition():    expect(1 + 1).toEqual(2)  def test_boolean():    expect(2 + 2 == 4).toBeTrue()  def test_concat():    expect("front" + "end").toEqual("frontend")  suite.register("1 + 1 = 2", test_addition)suite.register("2 + 2 == 4", test_boolean)suite.register("string concat", test_concat)  def main():    suite.run()  if __name__ == "__main__":    main()
目的:用 Python 实现一个微型测试运行器,完整模拟 Jest/Vitest 的“注册用例 → 执行断言 → 汇总结果 → 输出报告”核心机制,让你直观理解测试框架内部在做什么。
  1. class Suite:相当于 Jest 的全局容器,负责收集用例并统一执行。
  2. def register(self, name, fn):模拟 test() 注册用例,Jest 自动扫描 .test.js 文件后也做同样的事。
  3. except AssertionError as exc:断言抛错就是用例失败,与 Jest 捕获 expect 失败同理。
  4. def expect(actual):断言入口,返回的 dict 就是一组匹配器,类似 expect(x).toEqual(y)。
  5. assert actual == expected, ...深比较失败时抛出带提示的异常,输出定位信息。
  6. suite.register("1 + 1 = 2", test_addition)注册三条用例,对应三个单元测试。
  7. if __name__ == "__main__":保证被 import 时不执行,命令行运行才触发 main()。

常见误区

误区“我把覆盖率跑到 100%,这模块应该绝对安全了吧?那以后改它就不用写测试了吧?”

正解覆盖率只统计“代码被执行过”,不统计“执行结果被验证过”。一个测试完全可以执行到某行却不做任何断言——像尝了一口土豆但没咽下去,也不知道咸淡。真正要盯的是断言质量与分支覆盖,而不是把数字刷到 100%。覆盖率是台账,不是奖状。

为何常见:覆盖率是个醒目数字,容易把“过程指标”误当“目标”,人天生喜欢能打卡的量化目标。

误区“E2E 最贴近真实用户,那我每个功能都写 E2E,单元测试是不是可以省了?”

正解E2E 要启动浏览器、等待真实接口,慢且脆。一个细小文案改动就要跑整套菜单,像每道菜都要从点单到买单完整吃一遍。金字塔结构要求:大量便宜的单元测试打底,少量昂贵的 E2E 兜底。只吃全席不尝食材,成本会拖垮迭代。

为何常见:“真实”二字很有说服力,新手容易忽略速度与稳定性成本。

误区“测试要稳定,就应该把所有依赖都 mock 掉,这样永远不会因为网络挂掉。”

正解Mock 只适合替换“外部边界”,比如网络请求、时间、浏览器 API。自己模块间的协作逻辑如果也换成替身,等于把真食材全换成豆腐,最后测的是空气。主厨模拟蟹肉,但绝不会模拟自家灶台。

为何常见:刚学会 mock 时会有“掌控感”,觉得替身越多越可控,实际是测了一个假系统。

误区“快照失败了?肯定是我改对了,直接按 u 更新就行。”

正解快照失败只说明“输出变了”,不说明“变对了”。它是一张顾客视角的标准照,主厨必须比对 diff:是标签文案主动调整,还是样式意外回归?无脑更新等于自己给自己放水,回归就会被吞掉。

为何常见:更新快照最快消除红字,省去逐行看 diff 的功夫,惰性使然。

误区“我每次都先实现完再补单测,这不也是 TDD 吗?”

正解TDD 的顺序不可逆:先写失败的测试(红),再用最小实现让它通过(绿),最后重构。补测试发生在代码定型之后,测试只是“事后拍照”,无法驱动设计。就像吃完饭才写期望卡,菜已经端上桌了。

为何常见:大家习惯从“实现”出发,觉得有测试就行,忽略了“测试先行逼迫想清楚接口”这个核心价值。

面试题

Q1. 测试金字塔中数量最多、执行最快的是哪一层?

单选题 · easy

  • A.E2E测试
  • B.快照测试
  • C.单元测试← 正确答案
  • D.集成测试

答案:C。金字塔塔底最宽,放的是最多、最快、最便宜的单元测试;越往上越贵越少。E2E 在塔尖,快照是单测的一种工具,不属于金字塔层级。

Q2. 断言两个对象内容深度相等时,应优先使用哪个匹配器?

单选题 · easy

  • A.toThrow 检查抛错异常
  • B.toBe 判断引用相等
  • C.toMatch 匹配正则字符
  • D.toEqual 深比较对象内容← 正确答案

答案:D。就像尝两盘菜,toBe 只问“是不是同一盘端过来的”,toEqual 才会逐块比对配料。题目要求内容深度相等,正确答案是 toEqual;其他选项职责不同。

Q3. Vitest 相比 Jest 最核心的优势是什么?

单选题 · mid

  • A.独有更快的转译热更新
  • B.自动生成测试报告文档
  • C.内置更强大的断言匹配器
  • D.复用Vite配置并加速转译← 正确答案

答案:D。Vitest 生于 Vite 生态,直接复用 vite.config 与 esbuild 转译,省去 Jest 那套 Babel/ts-jest 配置,启动和热更新更快。A 是速度表象,本质仍是 D 的配置复用与转译管线。

Q4. 想在测试中替换 axios 模块,应使用哪个 API?

单选题 · mid

  • A.在钩子中重跑真实请求
  • B.使用 it.skip 跳过用例
  • C.使用 jest.mock 或 vi.mock← 正确答案
  • D.使用 describe 组织分组

答案:C。jest.mock / vi.mock 在模块加载层替换导出,是实现模块级替身的标准手段。beforeEach 只能辅助初始化,skip 是跳过用例,describe 只是分组,都改变不了真实请求。

Q5. 分支覆盖率衡量的是什么?

单选题 · hard

  • A.每个函数是否被调用过
  • B.每行代码是否被执行过
  • C.每个分支是否都被走到← 正确答案
  • D.每个文件是否都被加载过

答案:C。分支覆盖率专门盯 if/else、switch 等条件结构的真假路径是否都走过,对应后厨“两种火候都实操过”。函数覆盖率、行覆盖率、文件加载是另外的指标。

Q6. 下列适合使用快照测试的场景有?

多选题 · mid

  • A.组件渲染结构检查← 正确答案
  • B.接口返回数据对比← 正确答案
  • C.性能基准耗时测定
  • D.数据库备份恢复流程

答案:AB。快照适合稳定的序列化输出,组件 DOM 结构和接口数据都能拍“标准照”比对。性能需要专门基准工具,数据库备份属于后端运维,都不在前端快照能力范围内。

Q7. TDD 循环中,红灯之后的正确动作是什么?

单选题 · hard

  • A.提交代码到主干分支
  • B.最小实现使测试变绿← 正确答案
  • C.直接删除失败测试
  • D.重构使代码更加简洁

答案:B。红灯代表新期望刚写下且失败,此时唯一目标是写最小实现让它变绿,可以“作弊式”地简单。删除测试等于放弃期望,重构要等绿灯后做,提交更不是这一步的事。

Q8. 在 CI 中让测试真正成为质量门禁的做法有?

多选题 · easy

  • A.手工本地逐个点击页面
  • B.使用mock替代全部依赖
  • C.流水线中执行npm test← 正确答案
  • D.设置覆盖率阈值拦截构建← 正确答案

答案:CD。流水线执行 npm test 让每次提交都跑用例,覆盖率阈值不达标则退出码非零、构建变红,这两者构成硬门禁。手工点击依赖人肉且不可重复,mock 全部依赖则测了假系统。

Q9. 关于覆盖率阈值,正确的理解是什么?

单选题 · mid

  • A.依赖版本锁定方式
  • B.测试速度最高上限
  • C.代码质量最低门槛← 正确答案
  • D.快照更新自动流程

答案:C。覆盖阈值是“最低标准”——低于它就拦构建,相当于后厨规定“这道菜至少实操过几遍才能上菜单”。它不锁依赖、不限速、也不管快照更新流程。

Q10. 关于 E2E 测试,正确的说法有?

多选题 · hard

  • A.模拟完整用户操作流程← 正确答案
  • B.运行成本通常最高← 正确答案
  • C.适合替代所有单元测试
  • D.无需等待后端接口就绪

答案:AB。E2E 以顾客身份走完整点单上菜流程,最真实也最贵,所以放金字塔尖。它慢且脆,不能替代大量廉价的单元测试;通常还需要稳定的测试环境与接口,不是免依赖的银弹。