拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Vitest 可取消测试资源:用 test context 的 signal 在超时、bail 与 Ctrl+C 时释放资源

Vitest 可取消测试资源用 test context 的 signal 在超时、bail 与 CtrlC 时释放资源【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest测试代码经常持有不会自己停下来的资源一个fetch请求、一个子进程、一个文件流、一个轮询循环——当 Vitest 取消测试时这些资源并不会察觉worker 只能干等它们自己结束。Vitest 会在三种场景下取消测试测试超过timeout、--bail模式下另一个测试失败、或者你在终端按下了 CtrlC。从 Vitest 3.2.0 起test context 提供了一个signalAbortSignal在上述所有场景中都会被 abort——把它传给任何接受AbortSignal的 API资源就会随 Vitest 的取消动作一起被释放。本文完整讲解这一 recipe 的用法并结合 Vitest 运行器源码说明 signal 的创建、abort 时机与取消传播链路。哪些情况下 Vitest 会取消一个测试先明确signal的触发条件。综合 recipe 与 Test Context 文档以下四种情况都会让当前测试及其并发兄弟测试的signal进入 aborted 状态测试超时测试运行时间超过单测timeout参数或全局testTimeout配置手动取消用户在终端按 CtrlC程序化取消通过vitest.cancelCurrentRun显式取消整个测试运行bail 联动在并行运行时另一个测试失败且配置了bail默认1后续未开始的测试会被取消。不处理这些场景的常见后果超时后 worker 仍在等一个永远不会 resolve 的fetch测试文件挂死轮询循环持续打满本地服务的端口子进程泄漏。signal的价值就在于把Vitest 决定不再等这一决策同步传递给资源本身。核心模式把 signal 传给 fetchrecipe 给出的最小可用示例将测试的signal直接传给fetch的AbortSignal选项import { test } from vitest test(stop request when test times out, async ({ signal }) { await fetch(/heavy-resource, { signal }) }, 2000)如果 2 秒内请求没有完成fetch会以AbortError拒绝而不是让测试一直挂到请求自然结束。测试会干净地以超时失败worker 可以立刻进入下一个测试或退出。其他接受 AbortSignal 的 Web / Node APIsignal是一个标准的AbortSignal实例因此凡是需要外部取消能力的平台 API 都可以直接接受它fetch请求中途可被取消addEventListener传入{ signal }后abort 时监听器会被自动移除适合订阅到取消为止的测试ReadableStream.pipeTo管道传输可随取消中断Node.js APIfs.readFilefs/promises、child_process.spawn以及带{ signal }选项的setTimeout/setInterval——abort 时定时器会被清除任何自行实现取消逻辑的代码调用signal.throwIfAborted()或监听abort事件。这意味着同一个signal可以同时驱动请求、定时器和监听器资源清理是一次 abort、全部释放。转发 signal让取消向自定义 helper 传播真实项目里测试很少直接裸调fetch更多时候是在自己封装的 helper 里做轮询、重试或等待。正确的做法是把测试的signal作为参数一路传下去让取消能传播到最内层async function pollUntilReady(url: string, signal: AbortSignal) { while (!signal.aborted) { const res await fetch(url, { signal }) if (res.ok) { return } await new Promise(r setTimeout(r, 200)) } signal.throwIfAborted() } test(worker becomes ready, async ({ signal }) { await pollUntilReady(http://localhost:4000/health, signal) }, 5000)这个示例里有三个值得注意的细节循环条件检查signal.aborted每次轮询前先问一次还值得继续吗避免无意义的最后一次请求fetch(url, { signal })携带同一个 signal单次请求被取消时fetch会 rejectPromise 会向上抛出循环出口调用signal.throwIfAborted()这是一个防御性收口——如果signal.aborted在最后一次循环判断后变为 true这里会抛出 abort reason保证测试函数一定以取消错误退出而不是静默返回。对于不原生支持AbortSignal的第三方客户端例如某些 HTTP SDK可以在 helper 内部订阅signal的abort事件并手动调用客户端的close()/destroy()效果等价。源码解析Vitest 如何创建并 abort 这个 signalrecipe 讲的是怎么用运行器源码packages/vitest/src/runtime/runner/则解释了为什么它能覆盖所有取消场景。每个测试上下文持有独立的 AbortController在 context.ts 中createTestContext会为每个测试创建并挂载一个AbortController存放在TestContext与 controller 之间的WeakMap里const abortControllers new WeakMapTestContext, AbortController() export function createTestContext(test, runner): TestContext { // ... let abortController abortControllers.get(context) if (!abortController) { abortController new AbortController() abortControllers.set(context, abortController) } context.signal abortController.signal // ... }用WeakMap而非在 task 对象上加字段可以推断是为了保持 task 元数据的简洁同时让 controller 随 context 一起被 GC。统一的 abort 入口是abortContextSignalexport function abortContextSignal(context: TestContext, error: Error): void { const abortController abortControllers.get(context) abortController?.abort(error) }注意abort(error)把超时错误作为abort reason传入——也就是说signal.reason里携带的就是那条超时错误signal.throwIfAborted()抛出的也是它而不是一个裸的AbortError。测试函数的包装链withTimeout → withCancelsuite.ts 中每个测试 handler 在收集阶段就被层层包装withTimeout( withCancel(withAwaitAsyncAssertions(withFixtures(handler, { context }), task), task.context.signal), timeout, false, stackTraceError, (_, error) abortIfTimeout([context], error), )调用链是withTimeout起一个TaskDeadline计时器 → 超时触发onTimeout回调 →abortIfTimeout拿到测试 context →abortContextSignal(context, error)→AbortController.abort(超时错误)。随后两件事同时发生所有携带该signal的 APIfetch、spawn、定时器各自以取消结束资源被释放withCancel包装的 Promise 被 reject见下测试以取消错误失败。withCancel的实现非常短它在测试 Promise 上挂一个abort监听器一旦 abort 就立即reject(signal.reason)并在 Promise 正常 settle 后移除监听器。这保证了超时之后 Promise 不会被迟到的自然完成抢先 resolve——先到先得取消胜出。同文件中的onTestFailed/onTestFinished钩子超时同样会走abortController.abort(error)即钩子本身超时也会取消整个测试的 signal。run.ts 中aroundEach钩子超时的onTimeout也调用abortContextSignal(test.context, error)取消语义覆盖了 fixture 与 hook 层。手动取消与 bail 走的是另一条入口CtrlCstdin.ts 监听键盘输入调用ctx.cancelCurrentRun(keyboard-input)程序化取消core.ts 中的VitestNode.cancelCurrentRun(reason)负责向各 worker 池广播取消pools/rpc.ts中也有对应的vitest.cancelCurrentRun(reason)调用。这些入口最终都汇聚到 worker 侧对每个运行中测试的abortContextSignal调用因此无论是键盘、RPC 还是超时测试代码看到的都只是同一个signal变成了 aborted。仓库测试如何验证这些行为e2e 信号测试 用内联测试逐一验证了上述链路是很好的预期行为参照timeout aborts the signal without fixtures/timeout aborts the signal10ms 超时的测试中监听signal的abort事件并写task.meta断言 stderr 含Test timed out in 10ms.且meta为{ aborted: true }两个用例分别覆盖未使用 fixture和通过test.extend强制初始化 fixture两条路径timeout aborts all signals in concurrent teststest.concurrent.for([1,1,1])三个并发测试全部超时三个 signal 均被 abort——说明超时取消不是取消一个而是覆盖并发组内所有测试cancelling test run aborts the signal自定义 Reporter 在检测到console.log(ready)后调用this.vitest.cancelCurrentRun(keyboard-input)测试函数挂起的 Promise 被 abort 事件 resolve验证了程序化取消链路。小结从Vitest 3.2.0起{ signal }是 test context 的内置成员类型为AbortSignal它会在测试超时、CtrlC、cancelCurrentRun、bail触发时 abortsignal.reason携带取消原因如超时错误用法上只需把它传给fetch、addEventListener、pipeTo、fs.readFile、spawn、setTimeout等接受{ signal }的 API或在自己的轮询/等待 helper 中检查signal.aborted并在出口throwIfAborted()源码层面signal 由每个测试 context 独立的AbortController支撑context.tswithTimeout/withCancel包装链把超时与取消统一转化为 Promise 的 rejectsuite.ts。延伸阅读Test Context 的 signal 条目、bail配置、testTimeout配置、vitest.cancelCurrentRun API。【免费下载链接】vitestNext generation testing framework powered by Vite.项目地址: https://gitcode.com/GitHub_Trending/vi/vitest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门