ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

AVA 钩子限制详解:为什么 `t.snapshot()` 与 `t.try()` 只能用于测试

AVA 钩子限制详解:为什么 `t.snapshot()` 与 `t.try()` 只能用于测试 AVA 钩子限制详解为什么t.snapshot()与t.try()只能用于测试【免费下载链接】avaNode.js test runner that lets you develop with confidence 项目地址: https://gitcode.com/gh_mirrors/ava/ava导读在 AVANode.js 测试运行器中钩子test.before/test.after/test.beforeEach等承担着设置环境、清理资源等准备工作但它们并非测试本身。因此AVA 对钩子内可使用的断言与高级 API 施加了严格的限制——其中最具代表性的就是t.snapshot()与t.try()在钩子中调用会直接报错。本文基于 AVA 仓库中test/hook-restrictions测试套件及其快照报告结合lib/test.js、lib/assert.js的源码实现系统讲解这两条限制的触发条件、底层判定机制、错误消息格式以及如何正确地重构受影响的代码。读完本文你将能快速识别钩子内的非法用法并理解 AVA 为何这样设计。一、从快照报告看 AVA 如何固化钩子限制行为仓库中的快照报告文件 test/hook-restrictions/snapshots/test.js.md 记录了该测试套件的两条核心断言输出## snapshots cannot be used in hooks error message t.snapshot() can only be used in tests ## t.try() cannot be used in hooks error message t.try() can only be used in tests快照报告Snapshot Report是 AVA 快照测试机制的一部分测试运行时t.snapshot()会把实际值与.snap文件中保存的期望值比对报告.md文件则用于人类阅读与版本评审。这意味着上面两条错误消息是 AVA 官方通过快照测试固化的契约行为——任何改动导致消息文案或行为变化都会使快照测试失败。这为我们理解钩子限制提供了权威的行为基线。对应的测试定义位于 test/hook-restrictions/test.js它通过ava/test的fixture()辅助函数运行真实子进程中的故障 fixture并断言钩子确实失败、且失败消息与快照一致import test from ava/test; import {fixture} from ../helpers/exec.js; test(snapshots cannot be used in hooks, async t { const result await t.throwsAsync(fixture([invalid-snapshots-in-hooks.js])); const error result.stats.getError(result.stats.failedHooks[0]); t.snapshot(error.message, error message); }); test(t.try() cannot be used in hooks, async t { const result await t.throwsAsync(fixture([invalid-t-try-in-hooks.js])); const error result.stats.getError(result.stats.failedHooks[0]); t.snapshot(error.message, error message); });这里t.throwsAsync表明测试期望子进程以失败结束result.stats.failedHooks[0]则把错误定位到失败的钩子上——即错误确实是钩子抛出的而非测试体抛出。二、触发现场两份故障 fixture 逐行分析该测试套件通过两个最小化 fixture 复现非法用法位于test/hook-restrictions/fixtures/目录。2.1 在钩子中使用t.snapshot()test/hook-restrictions/fixtures/invalid-snapshots-in-hooks.jsimport test from ava; test.before(t { t.snapshot({}); }); test(cannot use snapshot in hook, t { t.pass(); });这段代码在test.before钩子内调用t.snapshot({})试图记录快照。运行结果钩子失败错误消息为t.snapshot() can only be used in tests。2.2 在钩子中使用t.try()test/hook-restrictions/fixtures/invalid-t-try-in-hooks.jsimport test from ava; test.before(async t { await t.try(tt tt.pass()); }); test(cannot use t.try() in hook, t { t.pass(); });这段代码在钩子内使用t.try()尝试创建尝试性测试。运行结果钩子失败错误消息为t.try() can only be used in tests。两个 fixture 的共同特征是非法调用都发生在test.before钩子中且钩子之外还有一个正常测试——这排除了测试文件整体无法运行的干扰确保失败点被精确锚定在钩子上。三、底层机制isHook标志与两条拦截分支要理解限制为什么存在需要看t对象是如何被构造的。每个测试/钩子执行时AVA 都会创建一个 ExecutionContext 实例把底层Test对象的能力映射为t上的方法。关键代码位于 lib/test.jsconstructor(test) { super({ pass() { test.countPassedAssertion(); return true; }, pending(promise, assertionStack) { test.addPendingAssertion(promise, assertionStack); }, fail(error) { return test.addFailedAssertion(error); }, failPending(error) { return test.failPendingAssertion(error); }, skip() { test.countPassedAssertion(); }, compareWithSnapshot: options test.compareWithSnapshot(options), experiments: test.experiments, disableSnapshots: test.isHook true, }); ... this.try async (...attemptArgs) { if (test.isHook) { const error new Error(t.try() can only be used in tests); test.saveFirstError(error); throw error; } ... }; ... }isHook的取值在 lib/test.js 由构造参数确定this.isHook options.isHook true;。也就是说AVA 在执行钩子与执行测试时构造的是同一个Test类只是钩子实例会带上isHook: true由此衍生出两条独立的限制分支API判定依据拦截位置抛出的错误t.snapshot()disableSnapshots: test.isHook truelib/test.jslib/assert.jsAssertionErrort.snapshot() can only be used in testst.try()if (test.isHook)显式判断lib/test.jslib/test.jsErrort.try() can only be used in tests3.1t.snapshot()的拦截disableSnapshots标志t.snapshot的实现位于 lib/assert.js。它在真正执行快照比较之前先检查disableSnapshotsthis.snapshot withSkip((expected, message) { if (disableSnapshots) { throw fail(new AssertionError(t.snapshot() can only be used in tests, { assertion: t.snapshot(), })); } assertMessage(message, t.snapshot()); ... });而disableSnapshots的唯一赋值来源正是构造 ExecutionContext 时传入的test.isHook truelib/assert.js 的构造参数默认值为false。因此在测试中isHook为false→disableSnapshots为false→ 快照断言正常执行在钩子中isHook为true→disableSnapshots为true→ 直接抛出AssertionError断言计为失败。从实现细节可以看出这不是忘了实现的漏洞而是通过构造参数显式注入的开关属于有意的 API 约束。3.2t.try()的拦截isHook显式分支t.try()用于在测试内执行尝试性测试attempt返回带commit()/discard()的结果对象以便在事务式语义下决定是否采纳本次尝试lib/test.js。它在方法体最前面就做了硬性检查this.try async (...attemptArgs) { if (test.isHook) { const error new Error(t.try() can only be used in tests); test.saveFirstError(error); throw error; } const {args, implementation, title} parseTestArgs(attemptArgs); ... };注意这里与t.snapshot()的两点差异错误类型不同t.try()抛出的是普通Errort.snapshot()抛出的是带assertion: t.snapshot()元数据的AssertionError错误登记方式不同t.try()在抛错前调用test.saveFirstError(error)把错误登记到该钩子的失败记录中因此测试报告里它显示为钩子失败而不是未捕获异常。3.3 为何如此设计快照与运行位置强绑定快照文件.snap按测试声明与标题组织快照的增删、更新--update-snapshots都以测试为粒度。钩子不产生独立的测试记录若允许在钩子中写快照会导致快照归属混乱、难以回归管理。将t.snapshot()限定在测试内保证了每个快照都能对应到唯一测试。尝试性测试是测试内的事务抽象t.try()的语义尝试运行、可选 commit/discard、可组合断言与快照记录天然面向测试体内部的逻辑分支如多条件探测、故障注入演练。钩子本身不参与测试结果的事务化因此限制其使用可以避免钩子悄悄引入未提交的尝试结果这类难以排查的状态污染。此外仓库文档 docs/02-execution-context.md 还提示了同类约束的另一例证t.teardown()同样cannot be used in hooks。这说明 AVA 在钩子边界上做了一整套系统性的 API 收敛而非对某个方法单独开洞。四、如何验证运行 hook-restrictions 测试套件如果你想在本地复现这一行为可执行npx ava test/hook-restrictions/test.js运行流程为ava/test的fixture()辅助函数见 test/helpers/exec.js以子进程方式启动invalid-snapshots-in-hooks.js/invalid-t-try-in-hooks.js子进程中的钩子抛出限制错误进程以失败状态退出父进程断言t.throwsAsync捕获失败并从result.stats.failedHooks[0]提取错误消息t.snapshot(error.message, error message)将消息与快照对比报告文件 test/hook-restrictions/snapshots/test.js.md 中记录的两条消息即最终契约。若修改了相关实现导致消息变化快照测试会失败提示你更新快照npx ava --update-snapshots——这正是快照机制对行为契约的守护作用。五、正确用法与代码重构建议了解了限制之后遇到钩子中需要快照/需要尝试执行的需求应如何改写5.1 需要快照时把逻辑挪进测试体// ❌ 错误快照不能用在钩子里 test.before(t { t.snapshot(setupResult); }); // ✅ 正确在测试内完成 let setupResult; test.before(() { setupResult prepare(); }); test(snapshot of setup result, t { t.snapshot(setupResult); });或者如果快照确实针对每个测试前的固定状态可在test.beforeEach中准备数据、在每个测试内各自断言快照保持快照与测试一一对应。5.2 需要t.try()时把探测逻辑放进测试体// ❌ 错误t.try() 不能用在钩子里 test.before(async t { await t.try(tt tt.pass()); }); // ✅ 正确在测试内执行尝试 test(try inside test, async t { const result await t.try(tt { tt.is(computeValue(), expected); }); result.commit(); // 或 result.discard(); });t.try()的返回值支持commit()/discard()两种收尾方式lib/test.js且在尝试中产生的断言、日志与快照记录会随提交/丢弃一并处理因此务必把它放在能真正决定是否采纳的测试上下文里。5.3 通用排查清单当你在钩子中看到与本文相同的两条错误消息时按序检查调用是否位于test.before/test.after/test.beforeEach/test.afterEach/test.afterEach.always等钩子回调内若在钩子中确实需要快照改用钩子准备数据 测试断言快照的结构若在钩子中需要尝试性验证把t.try()移入测试体并妥善处理commit/discard检查是否存在在钩子内调用其他包装函数、但包装函数内部间接使用了t.snapshot()/t.try()的间接违规——因为限制由isHook标志注入的t对象决定任何经由该t的间接调用同样会被拦截。六、小结本文以 AVA 仓库的 hook-restrictions 测试套件及其快照报告为线索完整还原了t.snapshot()与t.try()只能用于测试这一约束的行为契约与实现原理行为契约由 test/hook-restrictions/snapshots/test.js.md 中的两条快照消息固化任何改动都会触发快照测试失败实现机制分别落在两处t.snapshot()通过disableSnapshots: test.isHook truelib/test.js、lib/assert.js拦截t.try()通过if (test.isHook)显式分支lib/test.js拦截设计意图在于保持快照与测试的一一映射、维护t.try()事务语义的完整性并与t.teardown()的同类限制一起构成 AVA 在钩子边界上的系统性 API 收敛。对于 AVA 使用者而言把钩子负责环境准备、测试负责断言与探测作为铁律就能既安全地使用钩子又充分发挥快照与尝试性测试的能力。【免费下载链接】avaNode.js test runner that lets you develop with confidence 项目地址: https://gitcode.com/gh_mirrors/ava/ava创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表