Jest Setup 与 Teardown 钩子完全指南:beforeAll / afterAll / beforeEach / afterEach 用法与执行顺序详解

发布时间:2026/9/18 16:24:12
Jest Setup 与 Teardown 钩子完全指南:beforeAll / afterAll / beforeEach / afterEach 用法与执行顺序详解 Jest Setup 与 Teardown 钩子完全指南beforeAll / afterAll / beforeEach / afterEach 用法与执行顺序详解【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest导读在编写测试时通常需要在测试运行前完成一些准备工作如初始化数据库、建立连接并在测试运行后完成收尾工作如清理资源、断开连接。Jest 提供了beforeEach、afterEach、beforeAll、afterAll四类钩子函数来统一处理这些重复性工作。本文以 docs/SetupAndTeardown.md 为主干结合当前仓库中 Jest 两大测试运行器jest-circus 与 jest-jasmine2的源码实现系统讲解四种钩子的用法、作用域规则、异步支持、执行顺序以及故障排查建议。读完本文你将能够熟练编排测试生命周期理解钩子执行顺序背后的底层实现并写出干净、稳定、无共享状态污染的测试套件。一、为什么要使用 Setup 与 Teardown 钩子编写测试时经常出现这样的场景多个测试用例需要依赖同一份前置数据或外部资源同时每个用例结束后又需要把环境恢复原状。如果把这些逻辑重复写进每个用例代码会迅速膨胀且难以维护如果放在测试文件顶层一次性执行又无法满足每个用例独立环境的需求。Jest 针对这两种诉求分别提供了两类钩子重复性 Setup/TeardownbeforeEach每个测试前执行与afterEach每个测试后执行一次性 Setup/TeardownbeforeAll该文件或 describe 块内所有测试开始前执行一次与afterAll所有测试结束后执行一次。在仓库源码中这四类钩子由测试运行器统一收集、调度与执行。以默认运行器 jest-circus 为例钩子注册入口位于 packages/jest-circus/src/index.ts 的_addHook函数它通过dispatchSync({fn, hookType, name: add_hook, timeout})将钩子函数注册到当前 describe 块上四个对外 API 均复用这一注册逻辑// packages/jest-circus/src/index.ts节选 const beforeEach: THook (fn, timeout) _addHook(fn, beforeEach, beforeEach, timeout); const beforeAll: THook (fn, timeout) _addHook(fn, beforeAll, beforeAll, timeout); const afterEach: THook (fn, timeout) _addHook(fn, afterEach, afterEach, timeout); const afterAll: THook (fn, timeout) _addHook(fn, afterAll, afterAll, timeout);可以看到钩子注册时除了函数本身还接受一个可选的timeout参数详见后文超时控制一节。这些钩子随后会在运行阶段packages/jest-circus/src/run.ts被取出并调用。二、Repeating SetupbeforeEach 与 afterEach2.1 基础用法如果你有某些工作需要在多个测试前重复执行可以使用beforeEach钩子相应的收尾工作放到afterEach中。例如多个测试都要操作一个城市数据库每个测试前必须调用initializeCityDatabase()每个测试后必须调用clearCityDatabase()beforeEach(() { initializeCityDatabase(); }); afterEach(() { clearCityDatabase(); }); test(city database has Vienna, () { expect(isCity(Vienna)).toBeTruthy(); }); test(city database has San Juan, () { expect(isCity(San Juan)).toBeTruthy(); });beforeEach和afterEach与test一样支持异步代码支持两种写法接收done参数或直接返回 Promise。例如若initializeCityDatabase()返回一个 Promise则需要返回该 Promise 以便 Jest 等待它完成beforeEach(() { return initializeCityDatabase(); });若你更喜欢 async/await 风格也可以写成beforeEach(async () { await initializeCityDatabase(); })。更完整的异步测试写法可参考仓库文档 docs/TestingAsyncCode.md。2.2 底层执行模型从源码看beforeEach/afterEach在每个测试执行时被收集并调用。在 packages/jest-circus/src/run.ts 的_runTestInContext中const {afterEach, beforeEach} getEachHooksForTest(test); for (const hook of beforeEach) { if (test.errors.length 0) { // 如果某个 before 钩子已失败后续钩子不再执行 break; } await _callCircusHook({hook, test, testContext}); } await _callCircusTest(test, testContext); for (const hook of afterEach) { await _callCircusHook({hook, test, testContext}); }注意一个实现细节如果beforeEach钩子抛错导致test.errors.length 0那么剩余beforeEach以及测试主体都不会执行但afterEach仍会继续执行——这一设计保证了资源清理逻辑总能运行。此外从 packages/jest-circus/src/utils.ts 的getEachHooksForTest可以看出beforeEach钩子按从外层到内层的顺序收集外层先执行afterEach钩子按从内层到外层的顺序收集内层先执行这与下文第三节、第五节将要详述的执行顺序完全一致。三、One-Time SetupbeforeAll 与 afterAll在某些场景下你只需要在文件开头做一次性的 Setup。尤其当 Setup 是异步操作、无法内联完成时beforeAll与afterAll就派上了用场。继续用城市数据库的例子如果initializeCityDatabase()和clearCityDatabase()都返回 Promise且城市数据库可以在多个测试之间复用无需每个用例重建则可以改写为beforeAll(() { return initializeCityDatabase(); }); afterAll(() { return clearCityDatabase(); }); test(city database has Vienna, () { expect(isCity(Vienna)).toBeTruthy(); }); test(city database has San Juan, () { expect(isCity(San Juan)).toBeTruthy(); });在 jest-circus 中beforeAll与afterAll的执行发生在整个 describe 块级见 packages/jest-circus/src/run.ts 的_runTestsForDescribeBlockOnce——进入 describe 块后先依次调用beforeAll待块内所有测试含嵌套 describe 块运行完毕后再依次调用afterAllrun.ts。同时被标记为skip的 describe 块会跳过beforeAll/afterAll源码中if (!isSkipped)判断。一个值得注意的语义源码注释明确写道afterAll钩子不应影响测试状态通过或失败——参见 run.tstest_done事件会在afterEach之后、afterAll之前派发因此全局afterAll中即使抛错也不会阻塞后续测试的状态判定。四、Scoping钩子的作用域规则4.1 顶层钩子与 describe 内钩子文件顶层声明的before*/after*钩子对该文件中的每一个测试生效在某个describe块内声明的钩子仅对该 describe 块内的测试生效。例如我们不仅有城市数据库还有食物数据库可以为不同的测试做不同的 Setup// Applies to all tests in this file作用于本文件所有测试 beforeEach(() { return initializeCityDatabase(); }); test(city database has Vienna, () { expect(isCity(Vienna)).toBeTruthy(); }); test(city database has San Juan, () { expect(isCity(San Juan)).toBeTruthy(); }); describe(matching cities to foods, () { // Applies only to tests in this describe block仅作用于该 describe 块内测试 beforeEach(() { return initializeFoodDatabase(); }); test(Vienna 3 veal, () { expect(isValidCityFoodPair(Vienna, Wiener Schnitzel)).toBe(true); }); test(San Juan 3 plantains, () { expect(isValidCityFoodPair(San Juan, Mofongo)).toBe(true); }); });顶层beforeEach会在 describe 内的beforeEach之前执行。这一点在源码中有明确体现getEachHooksForTestpackages/jest-circus/src/utils.ts从测试节点逐级向上遍历父 describe 块将各层的beforeEach通过unshift压入结果头部从而保证外层的 beforeEach 永远先于内层执行而afterEach直接按遍历顺序内层先收集到追加从而保证内层的 afterEach 先于外层执行。4.2 完整执行顺序示例为了直观展示所有钩子的执行顺序考虑如下文件beforeAll(() console.log(1 - beforeAll)); afterAll(() console.log(1 - afterAll)); beforeEach(() console.log(1 - beforeEach)); afterEach(() console.log(1 - afterEach)); test(, () console.log(1 - test)); describe(Scoped / Nested block, () { beforeAll(() console.log(2 - beforeAll)); afterAll(() console.log(2 - afterAll)); beforeEach(() console.log(2 - beforeEach)); afterEach(() console.log(2 - afterEach)); test(, () console.log(2 - test)); });输出依次为1 - beforeAll 1 - beforeEach 1 - test 1 - afterEach 2 - beforeAll 1 - beforeEach 2 - beforeEach 2 - test 2 - afterEach 1 - afterEach 2 - afterAll 1 - afterAll可以归纳出几条规则顶层beforeAll最先运行顶层afterAll最后运行每个测试运行前外层beforeEach先于内层beforeEach每个测试运行后内层afterEach先于外层afterEach嵌套 describe 块自身的beforeAll在该块内第一个测试前触发afterAll在该块内最后一个测试后触发。上述顺序在 jest-circus 的单元测试中也有验证参见 packages/jest-circus/src/tests/hooks.test.ts含beforeEach 在嵌套 describe 中的执行顺序、同层多个 beforeEach 按声明顺序执行、beforeAll 的执行时机等用例。五、Order of Executiondescribe 与测试的执行次序Jest 会先执行测试文件中所有的 describe 回调收集阶段然后才执行真正的测试。因此Setup / Teardown 逻辑应该放在before*/after*钩子中而不是直接写在 describe 回调体内——describe 回调体内代码的执行时机与测试运行顺序无关容易造成混淆。收集阶段完成后默认情况下 Jest 按测试在收集阶段被遇到的顺序串行运行所有测试等待每个测试结束并完成清理后才进入下一个。考虑下面的示例文件及其输出describe(describe outer, () { console.log(describe outer-a); describe(describe inner 1, () { console.log(describe inner 1); test(test 1, () console.log(test 1)); }); console.log(describe outer-b); test(test 2, () console.log(test 2)); describe(describe inner 2, () { console.log(describe inner 2); test(test 3, () console.log(test 3)); }); console.log(describe outer-c); });输出describe outer-a describe inner 1 describe outer-b describe inner 2 describe outer-c test 1 test 2 test 3可以看到所有 describe 回调体中的console.log先于任何test输出且严格按声明顺序执行。5.1 依赖资源的按序 Setup 与 Teardown与describe和test一样Jest 按声明顺序调用before*钩子。但需要注意外层作用域的after*钩子先被调用即 after 钩子的执行顺序与声明顺序相反。利用这一特性可以实现资源的先建后拆、后建先拆式管理。例如连接connection依赖数据库database先建立连接再建立数据库清理时则先拆数据库再拆连接beforeEach(() console.log(connection setup)); beforeEach(() console.log(database setup)); afterEach(() console.log(database teardown)); afterEach(() console.log(connection teardown)); test(test 1, () console.log(test 1)); describe(extra, () { beforeEach(() console.log(extra database setup)); afterEach(() console.log(extra database teardown)); test(test 2, () console.log(test 2)); });输出connection setup database setup test 1 database teardown connection teardown connection setup database setup extra database setup test 2 extra database teardown database teardown connection teardown注意jasmine2 运行器如果你使用的是jasmine2测试运行器其after*钩子的调用顺序与声明顺序相同即为正向与 circus 的逆向不同。为了在上述例子中得到一致输出jasmine2 下应把 after 钩子按依赖关系反向声明beforeEach(() console.log(connection setup)); afterEach(() console.log(connection teardown)); beforeEach(() console.log(database setup)); afterEach(() console.log(database teardown)); - afterEach(() console.log(database teardown)); - afterEach(() console.log(connection teardown)); // ...这一差异可以从源码中找到依据jasmine2 的Suite实现在 packages/jest-jasmine2/src/jasmine/Suite.ts 中afterEach与afterAll均通过unshift插入数组头部因此后声明的 after 钩子反而排在前列执行时按数组顺序即逆声明序取出而 circus 在 packages/jest-circus/src/run.ts 中直接按收集顺序遍历afterEach收集顺序与声明顺序一致。换言之circus 是先声明先执行jasmine2 是后声明先执行跨运行器迁移测试时需留意。六、General Advice故障排查与最佳实践6.1 用 test.only 隔离单个测试如果某个测试失败了首先应该检查它单独运行时是否也会失败。将test临时改成test.only即可只运行这一个测试test.only(this will be the only test that runs, () { expect(true).toBe(false); }); test(this test will not run, () { expect(A).toBe(A); });在 jest-circus 源码中test.only对应mode: only的测试条目packages/jest-circus/src/run.ts 中通过hasFocusedTests判定聚焦模式未被标记only的测试会被跳过。运行完记得把test.only改回test否则 CI 中其他用例会被误跳过。6.2 利用 beforeEach 清理共享状态如果你有一个测试在整套测试运行时经常失败、但单独运行却通过那么大概率是其他测试污染了共享状态。此时通常可以通过在beforeEach中清理共享状态来解决如果还不确定是哪个共享状态被修改也可以在beforeEach中打印log相关数据辅助定位beforeEach(() { // 清理全局缓存、模块注册表、环境变量等共享状态 resetSharedState(); // 必要时打印当前状态帮助定位污染源 console.log(current state:, getSharedState()); });实践中常见的共享状态污染源包括全局变量、模块级缓存、时间/随机数可借助 jest 的 fake timers 控制参见 docs/TimerMocks.md、外部服务连接等。另外Jest 也提供了clearMocks、resetMocks、restoreMocks等自动化清理选项可参考 docs/Configuration.md 中对应的配置项说明。6.3 超时控制在 jest-circus 中钩子与测试共享同一套超时机制_callCircusHookpackages/jest-circus/src/run.ts中const timeout hook.timeout || getState().testTimeout;。也就是说若注册钩子时显式传入第二个参数如beforeAll(async () {...}, 30000)则使用该值作为该钩子的超时上限否则回退到全局的testTimeout配置默认 5000ms。因此耗时较长的 Setup例如启动 Docker 容器、拉取远程数据应显式调大钩子超时避免测试被误杀。七、总结四类钩子各司其职beforeAll/afterAll负责一次性文件级或 describe 块级Setup/TeardownbeforeEach/afterEach负责每个测试前后的重复性 Setup/Teardown。异步支持完备所有钩子都可以返回 Promise、使用 async/await 或接收done回调写法与异步测试一致。作用域可嵌套顶层钩子作用于整个文件describe 块内钩子只作用于块内测试beforeEach外层先执行、afterEach内层先执行beforeAll先于块内首个测试、afterAll晚于块内末个测试。执行顺序有章可循先收集全部 describe再串行运行测试before 钩子按声明顺序、after 钩子在外层作用域先触发circusjasmine2 运行器的 after 顺序存在差异跨运行器迁移需注意。调试有套路用test.only隔离单测、用beforeEach清理共享状态、为长耗时 Setup 显式设置超时。掌握这些钩子的语义与底层实现建议进一步阅读 packages/jest-circus/src/run.ts、packages/jest-circus/src/utils.ts 与 packages/jest-jasmine2/src/jasmine/Suite.ts你就能为自己的测试套件设计出干净、稳定、可维护的生命周期管理方案。【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考