首页
/ Supabase 仓库中的 Vitest 快照测试实战:文件快照、内联快照与更新机制全解析

Supabase 仓库中的 Vitest 快照测试实战:文件快照、内联快照与更新机制全解析

2026-09-04 21:10:46作者:董宙帆

本文基于当前仓库中 features-snapshots.md 快照测试参考文档展开,系统讲解 Vitest 快照测试的四种形态(文件快照、内联快照、文件对比快照、错误快照)、对象形状匹配、自定义序列化器、快照更新与路径定制等完整能力,并结合本仓库中 apps/docs 的 GraphQL schema 快照与 packages/ai-commands 的 SQL 格式化快照等真实用例,帮助你在大型 Monorepo 中建立稳定、可审查的快照测试实践。

快照测试的定位与本仓库中的真实用例

快照测试的核心思路是:捕获一段输出(字符串、对象、HTML、错误信息等),将其与存储的基准参照进行比对,任何输出变化都会在下次运行时导致测试失败。它特别适合验证"输出结构稳定"的场景——GraphQL schema、SQL 格式化结果、渲染产物、报错文案等。

在当前的 Supabase 仓库中,快照测试已经有多处落地。从源码检索可以看到,packages/pg-meta 的测试目录(如 tables.test.tsfunctions.test.tsviews.test.ts 等)以及 apps/docs/app/api/graphql/route.test.tspackages/ai-commands/src/sql/functions.test.ts 均使用了 toMatchSnapshot 等快照断言。仓库中实际存在两个已提交的 .snap 快照文件:

两个文件首行均为 // Vitest Snapshot v1,说明该仓库的快照版本化管理方式:快照文件必须提交进版本库,任何 schema 或输出格式的变化都会体现为 .snap 文件的 diff,从而在 code review 中被人眼审视。

文件快照:toMatchSnapshot 与 .snap 文件

最基础的用法是 toMatchSnapshot()

import { expect, test } from 'vitest'

test('snapshot', () => {
  const result = generateOutput()
  expect(result).toMatchSnapshot()
})

首次运行后,Vitest 会生成 __snapshots__/test.spec.ts.snap 文件,形如:

// __snapshots__/test.spec.ts.snap
exports['snapshot 1'] = `
{
  "id": 1,
  "name": "test"
}
`

后续每次运行都会把当前输出与该文件中的基准值比对,不一致即测试失败。

本仓库中这一模式最典型的用例是 route.test.ts 中的 /api/graphql schema snapshot 测试(第 80 行起):

describe('/api/graphql schema snapshot', () => {
  it('should match snapshot', async () => {
    const schemaQuery = `
        query {
          schema
        }
      `
    const request = new Request('http://localhost/api/graphql', {
      method: 'POST',
      body: JSON.stringify({ query: schemaQuery }),
    })

    const response = await POST(request)
    const json = await response.json()
    expect(json.errors).toBeUndefined()

    const {
      data: { schema },
    } = json
    expect(schema).toMatchSnapshot()
  })
})

该测试通过真实调用 Next.js 路由处理函数 POST,执行一个 { schema } 自省查询,然后把完整 schema 的字符串表示交给快照断言。对应的 route.test.ts.snap 文件有 255 行,完整记录了 GuideSearchResultSubsectionCollection 等类型定义及其字段文档注释。这意味着:任何人修改 docs 站 GraphQL schema(增删字段、调整类型、改动注释),CI 中的快照测试就会失败,迫使提交者在 review 阶段解释 schema 变更——这是对 API 契约的强约束。

同样基于 toMatchSnapshot 的还有 functions.test.ts(第 31、44 行对 formatSql(sql) 的结果做快照),其快照文件 functions.test.ts.snap 记录的内容是格式化后的 SQL 文本,例如:

exports[`debug > fix typos 1`] = `
"select
  *
from
  employees;"
`;

这里测试的是 ai-commands 包中"修复 SQL 拼写/格式化"相关函数,快照把期望的规范化输出固化下来,保证 AI 命令链路的 SQL 输出格式不会悄悄漂移。该包的运行环境配置可见 vitest.config.tsenvironment: 'node'testTimeout: 30000 并加载了 vitest.setup.ts

内联快照:toMatchInlineSnapshot

当基准值与测试代码强相关、且体量不大时,可以把快照直接写进测试文件:

test('inline snapshot', () => {
  const data = { foo: 'bar' }
  expect(data).toMatchInlineSnapshot()
})

首次运行(或按 u 更新)后,Vitest 会自动改写测试文件本身:

test('inline snapshot', () => {
  const data = { foo: 'bar' }
  expect(data).toMatchInlineSnapshot(`
    {
      "foo": "bar",
    }
  `)
})

内联快照的优点是"所见即所得":断言期望值就在测试代码旁边,code review 时不需要跳转到另一个 .snap 文件。适合小型、稳定的输出(如枚举值、配置对象);对于像 schema 这样的大体量输出,则更适合文件快照。

文件快照:toMatchFileSnapshot

当输出本身就是一个独立文件(HTML、JSON 等),或者你希望基准文件使用真实扩展名以便用对应工具查看时,可以使用 toMatchFileSnapshot 与显式文件比对:

test('render html', async () => {
  const html = renderComponent()
  await expect(html).toMatchFileSnapshot('./expected/component.html')
})

注意这是异步 API(await expect(...)),因为涉及文件读写。快照参考文档在"Key Points"中也明确建议:对大输出(HTML、JSON)优先使用 toMatchFileSnapshot,避免让 .snap 文件里挤满难以审查的巨型字符串。

快照提示(Hints):一个测试多个快照

同一测试中多次调用 toMatchSnapshot() 时,基准条目默认以 snapshot 1snapshot 2 编号区分,可读性差。传入第一个参数即可加描述性提示:

test('multiple snapshots', () => {
  expect(header).toMatchSnapshot('header')
  expect(body).toMatchSnapshot('body content')
  expect(footer).toMatchSnapshot('footer')
})

此时 .snap 文件中的键会变成 exports['multiple snapshots header 1'] 这样的形式,diff 时能直接看出是哪个部分变了。参考文档的 Key Points 亦强调:一个测试中有多份快照时,务必使用 hints

对象形状匹配:快照 + 非对称匹配器组合

快照要求精确匹配,但输出中往往有随机值(id、时间戳)无法稳定复现。Vitest 允许在 toMatchSnapshot 中传入"部分结构"参数,对不稳定字段改用非对称匹配器:

test('shape snapshot', () => {
  const data = {
    id: Math.random(),
    created: new Date(),
    name: 'test'
  }

  expect(data).toMatchSnapshot({
    id: expect.any(Number),
    created: expect.any(Date),
  })
})

含义是:id 只要求是 Number、created 只要求是 Date 实例,其余字段(如 name)仍与 .snap 基准精确比对。这是"既要结构精确、又要容忍随机值"的标准解法。

错误快照

错误对象同样可以快照化,断言抛错的文案格式稳定:

test('error message', () => {
  expect(() => {
    throw new Error('Something went wrong')
  }).toThrowErrorMatchingSnapshot()
})

test('inline error', () => {
  expect(() => {
    throw new Error('Bad input')
  }).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`)
})

第二个示例展示内联错误快照的存储形式 [Error: Bad input]。适合用于锁定用户可见的错误提示文案——例如 API 层的报错消息一旦写进快照,后续改动就必须在 review 中被显式确认。

更新快照:-u 与 watch 模式

快照基准更新只有两种官方途径:

# 更新所有快照
vitest -u
vitest --update

# 在 watch 模式下,按 'u' 键更新失败的快照
  • 命令行 vitest -u:将当前所有失败/缺失的快照一次性写回基准文件,通常用于"确认输出变化是预期行为"之后;
  • watch 模式下按 u:只更新当前失败的快照,粒度更细,适合本地迭代。

配套纪律来自参考文档的 Key Points:快照文件必须提交版本库,且快照变更要在 code review 中被专门审视-u 本身不产生测试价值,它只是把"人确认过的变化"固化为新基准。

自定义序列化器:snapshotSerializers

默认序列化对大多数 JS 值够用,但仓库里若有自定义类(带 toJSON、Date 包装、自定义类实例等),可以在序列化器里接管打印方式:

expect.addSnapshotSerializer({
  test(val) {
    return val && typeof val.toJSON === 'function'
  },
  serialize(val, config, indentation, depth, refs, printer) {
    return printer(val.toJSON(), config, indentation, depth, refs)
  },
})

test 判断该值是否走此序列化器,serialize 负责实际输出(可复用 printer 处理嵌套)。也可以在 Vitest 配置中按文件注册,对全项目生效:

// vitest.config.ts
defineConfig({
  test: {
    snapshotSerializers: ['./my-serializer.ts'],
  },
})

快照格式选项:snapshotFormat

全局调整快照的打印风格:

defineConfig({
  test: {
    snapshotFormat: {
      printBasicPrototype: false, // 不打印 Array/Object 原型
      escapeString: false,
    },
  },
})

printBasicPrototype 控制是否输出 Array [] / Object {} 这类原型标注,关掉后快照更紧凑;escapeString 控制字符串中的特殊字符转义方式。修改这类配置会让既有快照全部"失真",需要同步跑一次 vitest -u 重建基准——这也是它应写在配置里而不是散落在测试中的原因:全项目格式统一。

并发测试与上下文 expect

在并发(concurrent)测试中,全局 expect 的状态可能互相干扰,参考文档建议改用测试上下文注入的 expect

test.concurrent('concurrent 1', async ({ expect }) => {
  expect(await getData()).toMatchSnapshot()
})

test.concurrent('concurrent 2', async ({ expect }) => {
  expect(await getOther()).toMatchSnapshot()
})

这与 Vitest 的测试上下文(fixtures / context)机制一致:从 test 的第二个参数解构出的 expect 绑定到当前测试实例,避免并发调度下的快照状态串扰。

快照文件位置:resolveSnapshotPath

默认位置是 __snapshots__/<测试文件名>.snap,本仓库两个快照文件(route.test.ts.snapfunctions.test.ts.snap)均遵循该默认约定。若目录结构不同,可以用 resolveSnapshotPath 重写:

defineConfig({
  test: {
    resolveSnapshotPath: (testPath, snapExtension) => {
      return testPath.replace('__tests__', '__snapshots__') + snapExtension
    },
  },
})

实践要点汇总

综合参考文档的 Key Points 与本仓库的落地方式,快照测试的完整实践清单如下:

  1. 提交快照文件进版本库——.snap 是测试基准的一部分,不能生成后丢弃;
  2. 快照 diff 必须进 code review 流程——像 route.test.ts.snap 这样的 schema 快照,diff 即 API 变更说明;
  3. 一个测试多个快照时加 hints,让 .snap 键名自解释;
  4. 大输出用 toMatchFileSnapshot,保持 .snap 可审查;
  5. 内联快照自动回写测试文件,适合小而稳的输出;
  6. 不稳定字段用形状匹配expect.any(Number) 等)而非整体放宽;
  7. 并发测试使用上下文 expect
  8. 更新走 vitest -u 或 watch 模式 u,且必须在人确认变更合理之后执行。

本文全部内容以 .agents/skills/vitest/references/features-snapshots.md 为骨架,源码佐证均来自当前仓库内的实际测试文件与快照文件,可逐一打开核验。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384