
Jest 异步代码测试全指南Promises、Async/Await、回调与 .resolves/.rejects 的完整实践与底层原理【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest异步是 JavaScript 的常态网络请求、定时器、事件回调……当被测代码异步运行时Jest 必须确切知道测试何时才算真正完成否则就会出现测试已经结束断言却还没来得及执行的假通过。本文围绕 docs/TestingAsyncCode.md 的系统讲解覆盖 Jest 处理异步测试的四种主流方式——返回 Promise、async/await、done回调、.resolves/.rejects匹配器并结合本仓库packages/expect与packages/jest-circus的源码剖析 Jest 在底层是如何等待 Promise、识别done回调、设置超时并最终判定测试通过或失败的。读完本文你将掌握在不同异步风格下写出可靠、不产生误报的 Jest 测试的能力。为什么异步测试需要特殊处理Jest 在运行一个测试函数时默认会在函数体执行完毕后立即判定该测试完成。对于同步代码这没有问题但对于异步代码函数体内发起的异步操作如fetchData()返回的 Promise在函数返回时往往尚未 settle。如果 Jest 不被告知继续等待测试就会在断言真正运行之前结束产生两种典型误判误报通过断言从未执行测试却显示绿色误报失败异步回调中的错误逃逸到事件循环末尾导致Caught error after test environment was torn down之类的诡异报错。Jest 为此提供了多种显式机制让测试框架感知异步完成时机。从本仓库的测试覆盖看e2e/tests/asyncAndCallback.test.ts 与 e2e/tests/promiseAsyncHandling.test.ts 即专门用于验证这些异步风格的端到端行为可作为后续深入阅读的入口。方式一直接返回 Promise最基础的做法从测试函数中返回一个 PromiseJest 会等待它 resolve如果该 Promise 被 reject测试自动失败。test(the data is peanut butter, () { return fetchData().then(data { expect(data).toBe(peanut butter); }); });这里fetchData返回一个最终 resolve 为字符串peanut butter的 Promise。关键在于returnJest 拿到返回值后会将其视为异步完成的信号只有 Promise settle 之后才会继续判定测试结果。方式二async/await同样场景可以改写为async测试函数——在传给test的函数前加async关键字即可test(the data is peanut butter, async () { const data await fetchData(); expect(data).toBe(peanut butter); }); test(the fetch fails with an error, async () { expect.assertions(1); try { await fetchData(); } catch (error) { expect(error).toMatch(error); } });注意第二个用例由于fetchData()是预期要 reject的直接await会把 reject 转成同步 throw因此用try/catch捕获并断言错误内容同时通过expect.assertions(1)确保确实执行了一次断言——否则若fetchData意外 resolve没有进入 catch测试会因为零断言而看似通过而这恰恰不是我们想要的结果详见下文expect.assertions的验证价值。async/await与 Promise 写法本质上是同一逻辑的语法糖可依代码风格自由选用。方式三.resolves / .rejects 匹配器async/await还可以与.resolves、.rejects匹配器组合把解包 Promise 结果的样板代码交给 Jest 处理test(the data is peanut butter, async () { await expect(fetchData()).resolves.toBe(peanut butter); }); test(the fetch fails with an error, async () { await expect(fetchData()).rejects.toMatch(error); });在这两种写法中async/await同样是 Promise 示例所用逻辑的语法糖可以继续沿用也可以直接返回断言见下文.resolves/.rejects专节。一个必须记住的警告务必return或await这个 Promise——如果省略return/await测试会在fetchData返回的 Promise resolve 或 reject 之前就结束断言根本没有机会执行。方式四回调风格与 done 回调并非所有代码都基于 Promise。假设fetchData不接受返回值而是接收一个回调完成后调用callback(null, data)// 不要这样做 test(the data is peanut butter, () { function callback(error, data) { if (error) { throw error; } expect(data).toBe(peanut butter); } fetchData(callback); });默认情况下 Jest 测试在函数执行到结尾时就判定完成因此上面的测试会在fetchData完成回调尚未被调用时提前结束——问题在于测试在fetchData执行完的瞬间即结束根本等不到回调执行。正确做法是使用test的另一种形式为测试函数声明一个名为done的参数。只要测试函数接受参数Jest 就会等待done被调用后才结束测试test(the data is peanut butter, done { function callback(error, data) { if (error) { done(error); return; } try { expect(data).toBe(peanut butter); done(); } catch (error) { done(error); } } fetchData(callback); });done 回调的语义与陷阱永不调用done()→ 超时失败如果done一直未被调用测试会因超时而失败这正是我们想要的结果详见下文超时机制。断言失败时也要调用done(error)如果expect语句失败它会抛出错误导致done()不被执行。为了让测试日志展示真正的失败原因而不是一个不透明的超时错误必须把expect包进try块并在catch里把错误传给done。否则你只会得到一个看不懂的超时错误完全看不出expect(data)实际收到的值是什么。done与 Promise 不能混用如果同一个测试函数既接收done回调又返回 PromiseJest 会直接抛错。这是有意设计的预防措施用于避免测试中出现内存泄漏。// 错误示例同时使用 done 与返回值 test(the data is peanut butter, done { // ... return fetchData(); // Jest 会报错Test functions cannot both take a done callback and return something. });.resolves/.rejects专节更声明式的断言除了配合async/await.resolves/.rejects也可以独立使用在返回断言的测试中。.resolves会让 Jest 等待 Promise resolve再对结果执行后续匹配器若 Promise 被 reject测试自动失败test(the data is peanut butter, () { return expect(fetchData()).resolves.toBe(peanut butter); });同样必须return这个断言——省略return的话测试会在fetchData的 Promise resolve、then()有机会执行回调之前就结束。.rejects与.resolves完全对称等待 Promise reject 后对拒绝值执行匹配器若 Promise 意外 fulfill测试自动失败test(the fetch fails with an error, () { return expect(fetchData()).rejects.toMatch(error); });风格选择以上几种形式并没有哪一种绝对优于其他形式你可以在整个代码库中混用甚至在同一文件内混用——只需选择让测试读起来最简洁、最符合团队习惯的风格即可。例如连续异步步骤用async/await更易读单步断言结果/断言拒绝用.resolves/.rejects更精炼接入遗留回调 API 时用done更直接。源码视角Jest 底层是如何等待的理解了用法之后我们深入本仓库源码看看这些行为在实现层面是如何保证的。1. 测试函数如何被识别为异步在 packages/jest-circus/src/utils.ts 中Jest 通过检查函数参数个数来判断是否接受done回调function takesDoneCallback(fn: Circus.AsyncFn): fn is Global.DoneTakingTestFn { return fn.length 0; }fn.length是 JavaScript 函数声明形参个数的标准属性。因此测试函数声明了参数就等于测试期望收到done回调——这正是文档中使用带一个参数done的测试函数这一约定能生效的根本原因。2. 统一的异步执行器callAsyncCircusFn真正驱动测试/钩子执行的统一入口是 packages/jest-circus/src/utils.ts 中的callAsyncCircusFn。它返回一个 Promise并在内部做如下处理设置超时定时器先setTimeout一个以timeout为时长的定时器超时即reject(_makeTimeoutMessage(...))。默认超时时间在 packages/jest-circus/src/state.ts 中定义为testTimeout: 50005 秒可通过配置或test(name, fn, timeout)的第三参数覆盖。若接受done回调执行fn.call(testContext, done)Promise 只有在done被调用时才会 resolve/reject。若传入done(error)则以该错误 reject若done()被多次调用第二次起会报Expected done to be called once, but it was called multiple times.见 packages/jest-circus/src/utils.ts。若函数返回 Promise通过returnedValue.then(() resolve(), reject)把 Promise 的 settle 结果桥接到 Jest 的等待流程见 packages/jest-circus/src/utils.ts。若返回了 Promise/undefined 之外的值非钩子函数返回其他值会直接报错test functions can only return Promise or undefined.见 packages/jest-circus/src/utils.ts从源头杜绝返回了个寂寞的误用。done 与返回值冲突检测当done被调用时若发现函数还返回了非undefined的值会 reject 并提示Test functions cannot both take a done callback and return something.见 packages/jest-circus/src/utils.ts与文档中的警告一一对应。3. 超时错误信息的细节超时消息由_makeTimeoutMessage生成packages/jest-circus/src/utils.tsExceeded timeout of 5000 ms for a test while waiting for done() to be called. Add a timeout value to this test to increase the timeout, if this is a long-running test.注意while waiting fordone()to be called这一句只在测试函数接受done参数时出现——这正好帮助你在超时时快速区分是回调没被调用还是Promise 一直 pending。定时器在finally中被clearTimeout清理packages/jest-circus/src/utils.ts避免悬挂定时器阻塞 Node 进程退出。4. 测试结果的派发执行完成后packages/jest-circus/src/run.ts 中的_callCircusTest根据执行结果派发test_fn_success或test_fn_failure事件错误最终被收集到测试结果中而 packages/jest-circus/src/eventHandler.ts 在test_fn_start事件中会把seenDone重置为false确保每个测试的done计数互不干扰。5. expect.assertions 的验证价值文档反复强调记得加expect.assertions其实现位于 packages/expect/src/index.tsexpect.assertions(n)会把expectedAssertionsNumber写入 Jest 的全局断言状态在测试收尾时packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts 的_addExpectedAssertionErrors通过extractExpectedAssertionsErrors()读取该状态若实际断言数与期望不符则把错误注入测试结果。这正是文档所述否则一个 fulfilled 的 Promise 不会让测试失败的机制保证没有断言数校验fetchData().catch(...)中没有任何expect被调用时测试依然会被判定通过。6. .resolves / .rejects 的实现机制expect(fetchData())返回的 expectation 对象在 packages/expect/src/index.ts 中构建除普通匹配器外还会为每个匹配器注册resolves与rejects两个Promise 变体含各自的.not反向形式。以.resolves为例其核心逻辑makeResolveMatcherpackages/expect/src/index.ts做了两件事校验实参确实是 Promise先取actualWrapper若实参是函数则调用它获取返回值若非 Promise 直接抛错received value must be a promise or a function returning a promise挂接 then 回调actualWrapper.then(result ...)在 resolve 后把结果交给真正的匹配器执行若 Promise 被 reject则抛出一个带有Received promise rejected instead of resolved信息的错误使测试失败。.rejects的makeRejectMatcherpackages/expect/src/index.ts逻辑完全对称reject 时把错误值交给匹配器断言意外 resolve 时报Received promise resolved instead of rejected。因此从实现上可以确认文档中的两条行为承诺若 promise 被 rejected测试会自动失败.resolves与若 promise fulfilled测试会自动失败.rejects均得到了源码层面的强制保障。小结如何选择异步测试风格场景推荐写法关键注意点返回 Promise 的 APIreturn fetchData().then(...)必须return多步异步流程async () { await ...; expect(...) }预期 reject 时用try/catchexpect.assertions只想断言结果/拒绝值return expect(fetchData()).resolves.toBe(...)必须return断言Promise 方向错误会自动失败遗留回调 APIdone { ... done(); }断言要包try/catch永不调用会超时勿与返回值混用无论选择哪种风格核心原则只有一条让 Jest 明确知道异步工作何时结束——要么返回 Promise要么调用done。理解了 packages/jest-circus/src/utils.ts 中callAsyncCircusFn的统一处理逻辑你就能预测任何异步测试在 Jest 中的真实行为也能快速定位测试提前通过超时却不报错因这类常见疑难问题。更多相关配置可参考 docs/Configuration.md 中的testTimeout选项以及 docs/ExpectAPI.md 中expect.assertions的完整说明。【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考