Jest 快照测试(Snapshot Testing)深入指南:从快照生成、更新维护到最佳实践
Jest 快照测试Snapshot Testing深入指南从快照生成、更新维护到最佳实践【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest快照测试Snapshot Testing是 Jest 内置的一项核心能力用于防止 UI、API 响应、日志或错误消息等输出在无人察觉的情况下发生变化。本指南以 Jest 29.x 官方文档为主体结合本仓库中 examples/snapshot 的完整可运行示例与 packages/jest-snapshot 的源码实现系统讲解快照的生成原理、.snap文件结构、--updateSnapshot/-u更新机制、交互式更新模式、内联快照Inline Snapshots、属性匹配器Property Matchers以及最佳实践帮助你不仅会用更能理解其底层工作机制。快照测试的基本原理快照测试是一个非常实用的工具当你需要确保 UI 不会意外改变时。一个典型的快照测试用例会渲染一个 UI 组件、拍摄快照snapshot然后与存储在测试旁边的参考快照文件进行比对。如果两者不匹配测试就会失败——这可能意味着两件事之一要么改变是意外的代码有 bug 需要修复要么参考快照需要更新到组件的新版本。其核心思想是与其人工断言 UI 的每一个细节不如让 Jest 将渲染结果序列化并保存下来后续每次运行都自动与已保存的结果比对。从源码结构看这一整套逻辑集中在 packages/jest-snapshot/src/index.ts 中它对外导出了toMatchSnapshot第 157 行、toMatchInlineSnapshot第 214 行、toThrowErrorMatchingSnapshot第 427 行等匹配器以及SnapshotState来自 State.ts和addSerializer来自 plugins.ts。在 React 组件上使用快照测试对 React 组件可以采用类似的方法。与渲染完整图形 UI那需要构建整个应用不同你可以使用一个测试渲染器test renderer快速为 React 树生成一个可序列化的值。本仓库的 examples/snapshot 目录提供了一个完整的可运行示例。组件定义在 Link.js它根据pageprop 渲染一个a标签并在悬停时切换classNameimport {useState} from react; const STATUS { HOVERED: hovered, NORMAL: normal, }; export default function Link({page, children}) { const [status, setStatus] useState(STATUS.NORMAL); const onMouseEnter () setStatus(STATUS.HOVERED); const onMouseLeave () setStatus(STATUS.NORMAL); return ( a aria-label{status} className{status} href{page || #} onMouseEnter{onMouseEnter} onMouseLeave{onMouseLeave} {children} /a ); }对应的测试文件 link.test.js 如下使用testing-library/react渲染组件import renderer from react-test-renderer; import Link from ../Link; it(renders correctly, () { const tree renderer .create(Link pagehttp://www.facebook.comFacebook/Link) .toJSON(); expect(tree).toMatchSnapshot(); });第一次运行该测试时Jest 会创建一个快照文件 link.test.js.snap内容形如exports[renders correctly 1] a classNamenormal hrefhttp://www.facebook.com onMouseEnter{[Function]} onMouseLeave{[Function]} Facebook /a ;仓库中的实际快照文件顶部有一行版本头// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing这一行标识了快照文件的格式版本Jest 在解析时会校验它确保快照格式向后兼容。快照产物snapshot artifact应当与代码变更一起提交并在代码评审code review流程中接受审查。Jest 使用 packages/pretty-format 让快照在评审时可读性良好。在后续运行中Jest 会将渲染输出与之前的快照比对一致则测试通过不一致则失败——要么是测试运行器发现了代码本例中是Link组件中的 bug 需要修复要么是实现已经改变、快照需要更新。关于快照作用域的重要说明快照直接作用于你渲染的数据——在本例中是带有pageprop 的Link组件。这意味着即使其他文件比如App.js在使用Link时缺少了某些 props快照测试依然会通过因为该测试并不知道Link的其他使用场景它只限定在Link.js的渲染结果上在其他快照测试中用不同 props 渲染同一个组件也不会影响第一个测试因为各测试之间互不知晓。这种“按测试隔离、互不影响”的设计使得快照测试非常适合组件级别的回归防护。更新快照Updating Snapshots当 bug 引入导致快照测试失败时很容易发现修复问题确保快照测试重新通过即可。接下来讨论另一种情况——因为有意为之的实现变更导致快照测试失败。一种典型情形是我们故意改变示例中Link组件指向的地址// Updated test case with a Link to a different address it(renders correctly, () { const tree renderer .create(Link pagehttp://www.instagram.comInstagram/Link) .toJSON(); expect(tree).toMatchSnapshot(); });此时 Jest 会打印失败的 diff 输出显示新旧快照之间的差异。由于我们刚刚更新了组件指向的地址快照理应随之变化测试失败只是因为更新后的组件快照与旧的快照产物不再匹配。要解决这个问题需要更新快照产物。使用如下命令告诉 Jest 重新生成快照jest --updateSnapshot如果你更喜欢单字符形式也可以使用等价标志-u重新生成快照。这会为所有失败的快照测试重新生成快照产物。需要注意的是如果还有因非预期 bug 导致的其他失败快照测试应当先修复 bug 再重新生成快照避免把 bug 行为记录进快照。如果想限定只重新生成部分快照测试用例可以额外传入--testNamePattern标志只重录与模式匹配的测试jest --updateSnapshot --testNamePatternrenders correctly该功能也可以在 examples/snapshot 目录下直接体验修改Link组件后运行 Jest观察快照失败并执行更新命令。底层实现视角从实现看快照的“写入”与“校验”都由 State.ts 中的SnapshotState管理它维护已保存快照与本次运行接收到的新快照在--updateSnapshot时执行覆盖写回否则进行比对并记录失败。每个测试文件对应一个独立的快照状态因此 Jest 会为每个调用了toMatchSnapshot的测试文件生成一个独立的.snap文件——这与“快照按文件隔离”的行为一致。交互式快照模式Interactive Snapshot Mode失败的快照也可以在监听模式watch mode下交互式更新。运行jest --watch当有快照测试失败时按i键即可进入交互式快照模式Interactive Snapshot Mode。进入后Jest 会逐个测试地带着你过一遍失败快照让你有机会审查失败输出。在每个失败快照处你可以选择更新update该快照跳过skip进入下一个。全部处理完后Jest 会先给出一个汇总再返回监听模式。交互模式的核心价值在于你可以在更新前逐个确认每个差异是否符合预期而不是盲目地一次性接受所有变更。内联快照Inline Snapshots内联快照与外部快照.snap文件行为完全一致区别在于快照值会被自动写回源代码中。这样既能享受自动生成快照的便利又无需切换到外部文件去确认写入的值是否正确。示例首先编写一个调用.toMatchInlineSnapshot()不带参数的测试it(renders correctly, () { const tree renderer .create(Link pagehttps://example.comExample Site/Link) .toJSON(); expect(tree).toMatchInlineSnapshot(); });下次运行 Jest 时tree会被求值快照会作为参数写入toMatchInlineSnapshotit(renders correctly, () { const tree renderer .create(Link pagehttps://example.comExample Site/Link) .toJSON(); expect(tree).toMatchInlineSnapshot( a classNamenormal hrefhttps://example.com onMouseEnter{[Function]} onMouseLeave{[Function]} Example Site /a ); });仅此而已你同样可以通过--updateSnapshot更新内联快照或在--watch模式下按u键更新。两个实现细节值得注意默认情况下Jest 自己处理快照写入源代码的工作如果你的项目使用了 prettierJest 会检测到并将写入工作委托给 prettier包括遵循你的 prettier 配置以保证写入源码的快照字符串格式与项目风格一致内联快照的“写回源码”逻辑位于 InlineSnapshots.ts它负责将生成的快照文本精确地插入到调用toMatchInlineSnapshot()的参数位置。属性匹配器Property Matchers快照对象中经常包含运行时生成的字段比如 ID 和日期。如果直接快照这些对象会导致每次运行快照都失败it(will fail every time, () { const user { createdAt: new Date(), id: Math.floor(Math.random() * 20), name: LeBron James, }; expect(user).toMatchSnapshot(); }); // Snapshot exports[will fail every time 1] { createdAt: 2018-05-19T23:36:09.816Z, id: 3, name: LeBron James, } ;针对这些场景Jest 允许为任意属性提供非对称匹配器asymmetric matcher。这些匹配器在快照写入或比对之前被检查然后以匹配器而非实际值的形式保存到快照文件中it(will check the matchers and pass, () { const user { createdAt: new Date(), id: Math.floor(Math.random() * 20), name: LeBron James, }; expect(user).toMatchSnapshot({ createdAt: expect.any(Date), id: expect.any(Number), }); }); // Snapshot exports[will check the matchers and pass 1] { createdAt: AnyDate, id: AnyNumber, name: LeBron James, } ;任何不是匹配器的值都会被精确检查并原样保存到快照it(will check the values and pass, () { const user { createdAt: new Date(), name: Bond... James Bond, }; expect(user).toMatchSnapshot({ createdAt: expect.any(Date), name: Bond... James Bond, }); }); // Snapshot exports[will check the values and pass 1] { createdAt: AnyDate, name: Bond... James Bond, } ;处理字符串中的随机部分如果快照的对象是字符串而非对象则需要在测试前自行替换字符串中的随机部分。例如可以使用String.prototype.replace()配合正则表达式const randomNumber Math.round(Math.random() * 100); const stringWithRandomData div id${randomNumber}Lorem ipsum/div; const stringWithConstantData stringWithRandomData.replace(/id\d/, 123); expect(stringWithConstantData).toMatchSnapshot();除了字符串替换还有两种常用做法使用快照序列化器snapshot serializer对应配置项snapshotSerializers自定义快照的序列化输出使用mocking 机制mock 掉负责生成随机部分的库或方法。快照测试最佳实践快照是识别应用中意外接口变化的极好工具——无论这个接口是 API 响应、UI、日志还是错误消息。与任何测试策略一样要有效使用快照有一些最佳实践和准则需要遵守。1. 把快照当作代码对待提交快照并将其作为常规代码评审流程的一部分进行审查——把快照当作项目中任何其他类型的测试或代码一样对待通过让快照保持聚焦、简短并借助强制这些风格约定的工具确保快照可读如前所述Jest 使用pretty-format保证快照人类可读但你还可以引入额外工具来促进提交简短、聚焦的断言例如eslint-plugin-jest及其no-large-snapshots规则用于限制单个快照的大小或snapshot-diff的组件快照对比功能目标是让快照在 pull request 中易于审查并抵制“测试套件失败就直接重新生成快照”而不是追查失败根因的坏习惯。2. 测试应该是确定性的你的测试应当是确定性的在组件未变化的前提下多次运行相同的测试必须产生相同的结果。你有责任确保生成的快照不包含平台相关或其他非确定性数据。例如Clock.js 组件使用了Date.now()import {useEffect, useState} from react; export default function Clock() { const [seconds, setSeconds] useState(Date.now() / 1000); const tick () setSeconds(Date.now() / 1000); useEffect(() { const timerID setInterval(() tick(), 1000); return () clearInterval(timerID); }, []); return p{seconds} seconds have elapsed since the UNIX epoch./p; }如果直接快照Date.now()会让每次运行生成不同结果。原文档给出的方案是 mockDate.now()Date.now jest.fn(() 1_482_363_367_071);这样每次运行快照测试Date.now()都稳定返回1482363367071从而无论何时运行都生成相同的快照。而本仓库的 clock.test.js 给出了更现代的等价实现——使用 Jest 的 fake timers 直接设置系统时间jest.useFakeTimers().setSystemTime(1_482_363_367_071); it(renders correctly, () { const {container} render(Clock /); try { expect(container.firstChild).toMatchSnapshot(); } finally { cleanup(); } });生成的快照 clock.test.js.snap 稳定可预期exports[renders correctly 1] p 1482363367.071 seconds have elapsed since the UNIX epoch. /p ;3. 使用描述性强的快照名称始终努力为快照使用描述性强的测试名和/或快照名。最好的名称能描述预期的快照内容这会让评审者更容易在审查时核对快照也让任何人都能判断一个过时的快照在更新前是否代表正确行为。例如对比下面两组成对示例。模糊的命名exports[UserName / should handle some test case] null; exports[UserName / should handle some other test case] div Alan Turing /div ;描述性命名exports[UserName / should render null] null; exports[UserName / should render Alan Turing] div Alan Turing /div ;由于后者精确描述了输出中预期出现的内容当出错时更容易被发现exports[UserName / should render null] div Alan Turing /div ; exports[UserName / should render Alan Turing] null;常见问题解答FAQ快照会在 CI 系统上自动写入吗不会。自 Jest 20 起在 CI 系统上运行 Jest 时除非显式传入--updateSnapshot否则快照不会被自动写入。预期所有快照都是 CI 上运行的代码的一部分而由于新快照会自动通过它们不应通过 CI 上的测试运行。因此建议始终提交所有快照并纳入版本控制。快照文件应该提交吗应该。所有快照文件都应与其覆盖的模块及其测试一起提交它们应被视为测试的一部分其价值相当于 Jest 中任何其他断言。事实上快照代表了源模块在任意时间点的状态。这样当源模块被修改时Jest 就能指出与上一版本相比发生了什么变化。它还能在代码评审中提供大量额外上下文让评审者更好地研究你的改动。快照测试只适用于 React 组件吗React 和 React Native 组件确实是快照测试的典型用例相关入门教程见 TutorialReact.md但快照可以捕获任何可序列化的值只要目标是测试输出是否正确就应使用快照。Jest 仓库本身就是最好的证明它包含大量对 Jest 自身输出、断言库输出以及代码库各模块日志消息进行快照的示例例如对 e2e/tests/console.test.ts 中 CLI 控制台输出做快照的测试。快照测试与视觉回归测试visual regression testing有何区别两者是测试 UI 的两种不同方式服务于不同目的视觉回归测试工具对网页截图然后逐像素比对生成的图像快照测试将值序列化、存储在文本文件中并用diff 算法进行比较。二者各有不同的取舍这也是 Jest 选择构建快照测试而非视觉回归方案的原因。快照测试会取代单元测试吗不会。快照测试只是 Jest 内置的 20 多个断言之一。快照测试的目的不是取代现有单元测试而是提供额外价值、让测试更轻松。在某些场景下例如 React 组件快照测试可能消除对特定功能集合的单元测试需求但两者完全可以协同工作。快照测试在速度和生成文件大小方面的性能如何Jest 在设计时就以性能为重快照测试也不例外。由于快照存储在文本文件中这种测试方式快速且可靠。Jest 会为每个调用了toMatchSnapshot匹配器的测试文件生成一个新文件。快照的体积相当小作为参考Jest 代码库自身所有快照文件的总大小不到 300 KB。如何解决快照文件中的冲突快照文件必须始终代表其覆盖模块的当前状态。因此如果你合并两个分支时在快照文件中遇到冲突可以手动解决冲突或通过运行 Jest 并检查结果来更新快照文件。快照测试可以应用测试驱动开发TDD原则吗虽然可以手动编写快照文件但这通常不可行。快照的作用是帮助判断被测试模块的输出是否发生变化而不是在最初阶段指导代码设计。因此 TDD 的“先写测试、再写实现”循环并不适合快照测试。代码覆盖率code coverage能用于快照测试吗可以快照测试与任何其他测试一样支持代码覆盖率统计。小结快照测试的核心价值在于以极低的成本捕获“输出发生变化”这一事实。结合本仓库的 examples/snapshot 示例、packages/jest-snapshot 源码匹配器实现位于 index.ts、packages/pretty-format 序列化格式化以及 e2e/tests/console.test.ts 这类对自身输出做快照的测试你可以清楚地看到快照测试既适用于 React 组件也适用于任何可序列化输出。正确提交快照、保持确定性、使用描述性名称、按需更新快照就能让快照测试成为你测试体系中稳定可靠的防线。【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →