资讯动态

AVA 快照选择更新机制全解析:从 snapshot-workflow 测试报告看 --update-snapshots、--match 与行号选择

发布时间:2026/9/20 1:37:08 来源:尧图企业网站定制
AVA 快照选择更新机制全解析从 snapshot-workflow 测试报告看 --update-snapshots、--match 与行号选择【免费下载链接】avaNode.js test runner that lets you develop with confidence 项目地址: https://gitcode.com/gh_mirrors/ava/avaAVANode.js test runner的每一次快照断言都会产生两份文件.snap机器可读的压缩快照数据与.md人类可读的快照报告。本文以仓库中 test/snapshot-workflow/snapshots/selection.js.md 这份测试快照报告为主线拆解 AVA 在选择性更新快照场景下的真实行为t.snapshot.skip()、test.skip()跳过时旧快照如何保留、--match与行号选择如何做到只更新选中的测试并深入 lib/snapshot-manager.js 与 test/snapshot-workflow/selection.js 的源码与测试帮你彻底理解快照更新时的数据流向进而在真实项目中安全地批量更新快照。一、快照报告.md是什么一份可 diff 的变更日志根据 docs/04-snapshot-testing.mdAVA 为每个使用快照断言的测试文件生成两个文件*.snap保存实际快照数据仓库中实际是经过 gzip 压缩、由 lib/snapshot-manager.js 编码的二进制块后续比对依赖它*.md快照报告在更新快照时重新生成记录了每个测试块block下每个快照的序列化内容。如果把它提交进版本控制你可以直接通过 diff 观察快照发生了哪些变化。本文的主角 selection.js.md 正是运行 test/snapshot-workflow/selection.js 中 6 个场景测试后生成的报告。它的特殊之处在于报告里的每个 section 都记录的不是普通快照值而是快照报告前后版本的 diff 字符串——也就是报告的 diff 的报告用于验证 AVA 在选择性更新场景下确实按预期改动了快照。报告中出现了 4 个关键场景文件头部声明Generated by AVA每个场景以##标题 snapshot report diff段落组织--update-snapshots配合t.snapshot.skip()其他快照被更新--update-snapshots配合test.skip()其他测试的快照被更新--update-snapshots配合--match只有被选中的测试被更新--update-snapshots配合行号选择只有被选中的测试被更新。二、场景一t.snapshot.skip()与--update-snapshots报告第一个 section 展示的 diff 如下报告中换行以␊控制字符呈现这里为可读性还原为真实换行# Snapshot report for test.js The actual snapshot is saved in test.js.snap. Generated by AVA. ## foo Snapshot 1 { foo: one, } Snapshot 2 { - foo: two, two: something new, }对应的 fixture 是 test/snapshot-workflow/fixtures/skipping-snapshot-update/test.jstest(foo, t { if (process.env.TEMPLATE) { t.snapshot({foo: one}); t.snapshot({foo: two}); } else { t.snapshot.skip({one: something new}); t.snapshot({two: something new}); } });测试先以TEMPLATEtrue运行ava --update-snapshots生成初始状态两个快照{foo: one}、{foo: two}然后模拟用户修改把第一个快照改为t.snapshot.skip({one: something new})跳过第二个改为{two: something new}再执行ava --update-snapshots。结果与直觉一致跳过的快照原样保留Snapshot 1 仍是{foo: one}注意t.snapshot.skip()传入的参数{one: something new}被丢弃旧值被完整保留而未被跳过的快照被更新Snapshot 2 从{foo: two}变为{two: something new}。源码层面这一行为由 lib/snapshot-manager.js 的skipSnapshot()保证skipSnapshot({belongsTo, index, deferRecording}) { const oldBlock this.oldBlocksByTitle.get(belongsTo); const snapshot oldBlock?.snapshots[index] ?? {}; // Retain the label from the old snapshot, so as not to assume that the // snapshot.skip() arguments are well-formed. // Defer recording if called in a try(). if (deferRecording) { return () { // Must be called in order! this.recordSerialized({belongsTo, index, ...snapshot}); }; } this.recordSerialized({belongsTo, index, ...snapshot}); }关键点skipSnapshot()直接从旧快照块oldBlock.snapshots[index]中取出原数据重新记录完全无视t.snapshot.skip()传入的参数注释明言这是为了不假定 skip 参数格式正确。因此跳过 将旧快照原样搬运到新状态这也解释了为什么--update-snapshots下跳过的快照数据不会被清空或改写。补充块级跳过skipBlock()与单条快照跳过对应的还有块级跳过。同一文件中的skipBlock(title)lib/snapshot-manager.js在测试整体被跳过时执行若旧报告里有对应标题的块则整块原样放入新报告从而保留该测试的全部历史快照skipBlock(title) { const block this.oldBlocksByTitle.get(title); if (block) { this.newBlocksByTitle.set(title, block); } }三、场景二test.skip()与--update-snapshots报告第二个 section# Snapshot report for test.js The actual snapshot is saved in test.js.snap. Generated by AVA. ## foo Snapshot 1 { foo: one, } ## bar Snapshot 1 - { - bar: one, - } [ something new, ]fixture 是 test/snapshot-workflow/fixtures/skipping-test-update/test.js(process.env.TEMPLATE ? test : test.skip)(foo, t { t.snapshot(process.env.TEMPLATE ? {foo: one} : [something new]); }); test(bar, t { t.snapshot(process.env.TEMPLATE ? {bar: one} : [something new]); });这个场景与场景一形成对照被跳过的整个测试test.skip()的foo其快照{foo: one}被完整保留走的是skipBlock路径而未被跳过的测试bar的快照正常更新为[something new]。两个场景合起来构成了一条明确的行为契约跳过单条快照断言t.snapshot.skip()→ 仅保留该条快照旧数据跳过整个测试test.skip()或test.only()之外的测试→ 保留该测试块下所有快照旧数据其余未跳过的内容在--update-snapshots下正常更新。这保证了在更新快照时你临时用 skip 关掉的断言不会因为旧快照还在报告里但测试不再运行而误删数据也不会被异常参数污染。四、场景三--update-snapshots--match精确圈定更新范围报告第三个 section# Snapshot report for test.js The actual snapshot is saved in test.js.snap. Generated by AVA. ## foo Snapshot 1 { - foo: one, foo: new, } ## bar Snapshot 1 { bar: one, }fixture 是 test/snapshot-workflow/fixtures/select-test-update/test.jstest(foo, t { t.snapshot(process.env.TEMPLATE ? {foo: one} : {foo: new}); }); test(bar, t { t.snapshot(process.env.TEMPLATE ? {bar: one} : {bar: new}); });对应的测试用例test/snapshot-workflow/selection.js执行命令为ava --update-snapshots --match foo于是只有标题匹配foo的测试被执行并更新{foo: one}→{foo: new}bar的快照则保持原样。这正是 docs/04-snapshot-testing.md 中建议的做法If you need to update snapshots for only a particular test, you can use--update-snapshotstogether with e.g.--matchor.only()to select the test.--match的使用要点根据 docs/05-command-line.md命令形式ava --matchpattern-m可重复传入多个模式匹配基于测试标题使用通配符大小写不敏感--match*foo标题以foo结尾--matchfoo*标题以foo开头--match*foo*标题包含foo--matchfoo标题恰好等于foo仍不区分大小写--match!*foo*标题不包含foo--matchfoo*bar标题以foo开头且以bar结尾--matchfoo* --match*bar多个模式取并集--match优先级高于.only()修饰符只有带显式标题的测试会被匹配无标题或标题由实现函数推导的测试在使用--match时会被跳过。因此--update-snapshots --match foo的语义是先按标题圈定要跑的测试再只更新这些测试产生的快照未选中的测试及其快照保持不动——避免了一次-u把全部快照无差别刷新带来的误更新风险。五、场景四--update-snapshots 行号选择报告第四个 section 与场景三的 diff 内容一致同样是foo被更新、bar保持不变但圈定测试的方式不同——这次使用的是行号定位# Snapshot report for test.js The actual snapshot is saved in test.js.snap. Generated by AVA. ## foo Snapshot 1 { - foo: one, foo: new, } ## bar Snapshot 1 { bar: one, }对应的测试用例test/snapshot-workflow/selection.js执行命令为ava --update-snapshots test.js:3-5行号选择是 AVA 位置参数positionalpattern的内置能力。根据 docs/05-command-line.md 的参数说明pattern 除了 glob、目录和文件路径外还支持文件路径 冒号 逗号分隔的 1-based 行号或区间来精确定位测试例如ava test.js:4,7-9即只运行test.js中声明在第 4 行、以及第 79 行foo测试所在的区间的测试。结合--update-snapshots效果与--match相同只更新被行号选中的测试所对应的快照块其余快照保持原样。这在编辑器中右键运行该测试或按代码位置批量重录快照时尤其顺手。六、测试如何验证更新结果beforeAndAfter 宏与快照报告的 diff这份报告并不是手工编写的文档而是由 test/snapshot-workflow/selection.js 通过宏 test/snapshot-workflow/helpers/macros.js 自动生成、并作为断言的一部分固化下来的。理解这个宏就理解了报告里每个 section 的来历export async function beforeAndAfter(t, {cwd, expectChanged, env {}, cli []}) { const updating process.argv.includes(--update-fixture-snapshots); if (updating) { // 1. 先以 TEMPLATEtrue 生成 fixture 的初始状态 await fixture([--update-snapshots], { cwd, env: {TEMPLATE: true, AVA_FORCE_CI: not-ci}, }); } const before await readSnapshots(cwd); // 2. 把 fixture 复制到临时目录 await withTemporaryFixture(cwd, async cwd { // 3. 用真实 CLI 参数运行 fixture await fixture(cli, {cwd, env: {AVA_FORCE_CI: not-ci, ...env}}); const after await readSnapshots(cwd); if (expectChanged) { t.not(after.report, before.report, expected .md to be changed); t.notDeepEqual(after.snapshot, before.snapshot, expected .snap to be changed); t.snapshot(cleanStringDiff(before.report, after.report), snapshot report diff); } else { t.is(after.report, before.report, expected .md to be unchanged); t.deepEqual(after.snapshot, before.snapshot, expected .snap to be unchanged); } }); }流程要点初始化以TEMPLATEtrue npx ava --update-snapshots在 fixture 目录生成初始.snap与.md见 test/snapshot-workflow/README.md 中 Invariants 一节——所有共用 fixture 的测试必须用同一种方式初始化否则会互相覆盖初始状态读取基线readSnapshots()读取test.js.snap并解压同时读取test.js.md报告文本解压逻辑直接调用 lib/snapshot-manager.js 导出的extractCompressedSnapshot在临时副本上运行withTemporaryFixture将 fixture 复制到临时目录避免污染仓库内的初始状态然后用目标 CLI 参数如[--update-snapshots, --match, foo]运行断言结果expectChanged: true时断言.md报告文本确实变化、.snap数据确实变化并用concordance.diff(before.report, after.report)计算两份报告的文本 diff去掉␊控制字符后作为快照值记录——这就是 selection.js.md 中每个snapshot report diffsection 的由来expectChanged: false时断言报告与快照数据完全不变。由此可知selection.js.md本质是针对报告文本 diff 的快照它既验证了 AVA 的选择性更新逻辑本身也是快照报告格式的一个真实样例。关于报告中的␊字符在 selection.js.md 的原始内容里diff 文本中的换行被渲染为␊U240A行末符号。这是因为报告的渲染逻辑把字符串快照中的真实换行转义为可见控制字符在cleanStringDiff()中则先replaceAll(␊, )清理后再比较避免换行被重复转义。阅读原始报告时看到␊直接理解为换行即可。七、底层机制save() 与原子写入无论哪种更新路径最终都要落到 lib/snapshot-manager.js 的save()。核心逻辑async save() { const {dir, relFile, snapFile, snapPath, reportPath} this; if (this.updating this.newBlocksByTitle.size 0) { return { changedFiles: [cleanFile(snapPath), cleanFile(reportPath)].flat(), temporaryFiles: [] }; } if (!this.hasChanges) { return null; } const snapshots { blocks: sortBlocks(this.newBlocksByTitle, this.blockIndices).map(([title, block]) ({title, ...block})), }; const buffer encodeSnapshots(snapshots); const reportBuffer generateReport(relFile, snapFile, snapshots); await fs.promises.mkdir(dir, {recursive: true}); const temporaryFiles []; const tmpfileCreated file temporaryFiles.push(file); await Promise.all([ writeFileAtomic(snapPath, buffer, {tmpfileCreated}), writeFileAtomic(reportPath, reportBuffer, {tmpfileCreated}), ]); return {changedFiles: [snapPath, reportPath], temporaryFiles}; }几个值得注意的工程细节无变化不落盘hasChanges为false时save()直接返回null避免无谓写入.snap 与 .md 同步原子写入两个文件通过writeFileAtomic并行写入保证磁盘上不会出现数据已更新但报告未更新的中间态顺序稳定sortBlocks(this.newBlocksByTitle, this.blockIndices)按块在测试文件中的声明顺序block index排序输出保证报告可稳定 diff——这由report-declaration-order等测试覆盖见 test/snapshot-order/报告与数据同源generateReport()与encodeSnapshots()都基于同一份snapshots对象确保.md与.snap永远一致。八、实战安全地选择性更新快照结合文档与源码验证你在真实项目中有四种精确更新策略场景推荐命令效果更新全部快照npx ava --update-snapshots或-u所有运行中测试的快照被重录更新部分测试按标题npx ava --update-snapshots --match foo*仅标题匹配的测试快照被更新更新部分测试按位置npx ava --update-snapshots test.js:3-5仅指定行号区间内的测试快照被更新更新部分测试按修饰符在目标测试上加.only()后npx ava --update-snapshots仅.only()测试被运行并更新注意--match优先级高于.only配套的跳过保护机制用t.snapshot.skip(...)临时跳过单条断言-u时该条旧快照数据被完整保留参数内容被丢弃见 lib/snapshot-manager.js用test.skip(...)临时跳过整个测试-u时该测试块的全部旧快照被保留skipBlock路径见 lib/snapshot-manager.js快照报告.md建议提交进版本控制每次-u后通过 git diff 逐条审查快照变更再决定是否保留——这正是 AVA 设计.md报告的初衷。需要注意的是--update-snapshots只能作用于实际运行了的测试被--match、行号、.only()排除的测试不会运行它们的快照自然也不会被触碰。若某测试被跳过skip其旧数据会被保留而非删除若某个快照断言被彻底移除且未跳过更新时对应旧快照会被清理相关行为由 test/snapshot-workflow/removing-snapshots/ 等 fixture 覆盖。九、延伸阅读快照测试完整入门与存储位置规则docs/04-snapshot-testing.md含snapshotDir配置与 TypeScript 预编译场景CLI 完整参数与--match模式语法docs/05-command-line.md快照工作流测试套件的整体设计fixture 初始化约定、串行执行原因test/snapshot-workflow/README.md快照管理器核心实现skip、record、save、原子写入、源映射解析lib/snapshot-manager.js更多快照场景 fixture新增、删除、重排、标题变更、无效快照文件等test/snapshot-workflow/fixtures/快照顺序稳定性与随机性测试test/snapshot-order/【免费下载链接】avaNode.js test runner that lets you develop with confidence 项目地址: https://gitcode.com/gh_mirrors/ava/ava创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价