
jest-circus 事件驱动测试运行器从事件模型到自定义环境扩展的完整指南【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest导读jest-circus是 Jest 的下一代测试运行器next-gen test runner自 Jest 27 起已成为 Jest 的默认测试运行器。它采用基于 Flux 架构的事件分发模型event-driven/flux-based将测试声明、Hook 执行、测试运行与结果汇总解耦为一系列可订阅的事件开发者可以在自定义测试环境中通过handleTestEvent订阅并观察测试运行的全过程。读完本文你将掌握jest-circus的安装配置、事件体系同步事件与异步事件、状态对象结构并能基于自定义测试环境编写事件处理器实现自定义报告、超时统计、失败诊断等扩展能力。一、jest-circus 是什么Flux 风格的事件驱动架构从官方定义看Circus 是一个基于 Flux 架构的 Jest 测试运行器其设计目标是快速fast、可维护maintainable、易于扩展simple to extend。所谓 Flux 风格体现在 state.ts 的核心实现中单一状态源State整个测试运行期间的所有状态当前 describe 块、正在运行的测试、未处理的错误、随机种子等被集中存放在globalThis[STATE_SYM]指向的 State 对象中通过getState()/setState()读写resetState()重置事件分发dispatch所有行为变更都以事件形式派发dispatch(event)依次调用所有已注册的事件处理器await handler(event, getState())同步事件则走dispatchSync处理器链Handlers默认注册了eventHandler核心状态机与formatNodeAssertErrors断言错误格式化两个处理器用户可以通过addEventHandler/removeEventHandler增删自定义处理器。// packages/jest-circus/src/state.ts节选 const handlers: ArrayCircus.EventHandler globalThis[EVENT_HANDLERS] || [ eventHandler, formatNodeAssertErrors, ]; export const dispatch async (event: Circus.AsyncEvent): Promisevoid { for (const handler of handlers) { await handler(event, getState()); } }; export const dispatchSync (event: Circus.SyncEvent): void { for (const handler of handlers) { handler(event, getState()); } }; export const addEventHandler (handler: Circus.EventHandler): void { handlers.push(handler); };也就是说谁在什么时候执行什么完全由事件流驱动describe/test/beforeEach等 API 在声明时只负责派发同步事件见 index.ts 中_addTest、_addHook对dispatchSync的调用真正决定测试如何运行的逻辑则集中在 run.ts 的run()中——它派发run_start、遍历根 describe 块、按顺序执行 hook 与测试、最终派发run_finish并汇总RunResult。二、事件订阅在自定义环境中使用 handleTestEventCircus 允许开发者通过自定义测试环境custom environment上的可选事件处理器绑定到这些事件上。所谓自定义环境即配置项testEnvironment指向的自定义类它继承自jest-environment-node或jest-environment-jsdom提供的基础环境。在 jest-environment-node 的基类中handleTestEvent被定义为可覆写的空实现Circus 在运行时会调用环境实例上的该方法并把事件对象与状态对象一起传入。官方 README 给出的标准用法如下import type {Event, State} from jest-circus; import {TestEnvironment as NodeEnvironment} from jest-environment-node; class MyCustomEnvironment extends NodeEnvironment { //... async handleTestEvent(event: Event, state: State) { if (event.name test_start) { // ... } } }代码中Event与State两个类型从jest-circus导出对应 index.ts 中的export type Event Circus.Event; export type State Circus.State;其完整定义位于仓库的 packages/jest-types/src/Circus.ts。事件与状态数据的只读约定阅读事件模型时需要注意两个官方明确声明的约定不支持修改事件或状态数据在handleTestEvent中修改event或state属于未支持行为可能导致意外行为甚至在未来某个版本中无警告地失效不构成破坏性变更新增事件、新增事件字段或状态字段不会被视为破坏性变更可能在任何 minor 版本中出现。这意味着插件代码应只依赖文档化的既有事件不要假设事件集合固定不变。同步事件例外不是所有事件都会等待Circus 默认会暂停执行直到handleTestEvent返回的 Promise 被 resolve即处理器可以异步地观察每个事件。但以下同步事件不遵循该规则出于向后兼容原因与process.on(unhandledRejection, callback)的签名限制有关start_describe_definitionfinish_describe_definitionadd_hookadd_testerror这些事件在 Circus.ts 中被归类为SyncEvent通过dispatchSync派发处理器无法通过返回 Promise 来延迟这些事件的处理流程。对大多数使用场景而言这通常不会造成问题——它们大多是声明阶段的同步操作注册 describe、hook、test而不是真正耗时的运行阶段事件。三、完整事件清单同步事件与异步事件要编写可靠的事件处理器需要掌握完整的事件类型。以下依据 packages/jest-types/src/Circus.ts 整理。3.1 同步事件SyncEvent同步事件携带的数据极少且处理器不能通过 Promise 延迟执行事件名携带字段触发时机start_describe_definitionblockName、mode、asyncError开始注册一个describe块finish_describe_definitionblockName、mode结束注册一个describe块add_hookhookType、fn、timeout、asyncError注册beforeAll/beforeEach/afterEach/afterAlladd_testtestName、fn、mode、concurrent、timeout、failing、asyncError注册一个测试用例errorerror、promise?发生在测试/Hook 之外的未处理错误error_handledpromise之前未处理的 Promise 被后续处理其中mode类型为void | skip | only | todo对应test.skip、test.only、test.todo等修饰符concurrent标识该测试是否通过test.concurrent声明failing标识是否通过test.failing声明期望失败的测试。3.2 异步事件AsyncEvent异步事件覆盖了测试运行的完整生命周期处理器返回的 Promise 会被等待事件名关键负载语义setuptestNamePattern?、runtimeGlobals、parentProcess第一个派发的事件适合初始化各类设置同时注入全局错误处理器include_test_location_in_result—在结果中包含测试位置run_start/run_finish—整个文件测试运行开始 / 结束run_describe_start/run_describe_finishdescribeBlock某个 describe 块开始 / 结束运行hook_starthookHook 开始执行hook_success/hook_failurehook、describeBlock?、test?、error?Hook 执行成功 / 失败test_starttest单个测试开始包括其所有 hook 与测试函数test_fn_starttest仅测试函数本身开始执行test_fn_success/test_fn_failuretest、error?测试函数成功 / 失败test_retrytest测试失败后即将重试describe_retrydescribeBlockdescribe 块整体重试describe.retrytest_startedtest测试实际开始未跳过test_skip/test_todotest测试被跳过 / 标记为 todotest_donetest测试及其全部 hook 均运行完毕状态最终确定concurrent_tests_start/concurrent_tests_endtests、describeBlock一组并发测试test.concurrent开始 / 结束teardown—一切结束、即将向上层返回结果前的收尾事件恢复全局错误处理器3.3 状态对象State的关键字段State在 Circus.ts 中定义处理器可以通过它读取当前运行上下文type State { currentDescribeBlock: DescribeBlock; // 当前正在注册的 describe 块 currentlyRunningTest?: TestEntry | null; // 当前正在运行的测试含 hook 执行期间 hasFocusedTests: boolean; // 是否存在 test.only hasStarted: boolean; // 是否已开始运行 parentProcess: Process | null; // 外层 process 对象 randomize?: boolean; // 是否随机执行randomize 配置 rootDescribeBlock: DescribeBlock; // 根 describe 块 seed: number; // 随机种子 testNamePattern?: RegExp | null; // -t 名称过滤模式 testTimeout: number; // 默认超时默认 5000ms maxConcurrency: number; // 并发测试最大并发数默认 5 unhandledErrors: ArrayException; // 未处理错误列表 // ...含 describe 重试选项、未处理拒绝错误映射等 };这些默认值testTimeout: 5000、maxConcurrency: 5可以在 state.ts 的createState()中直接看到。四、安装与配置4.1 安装注意自 Jest 27 起jest-circus已是 Jest 的默认测试运行器因此使用 Jest 时无需单独安装即可直接使用。如需在旧版本项目或独立场景下显式安装可通过 yarnyarn add --dev jest-circus或通过 npmnpm install --save-dev jest-circus4.2 配置 testRunner通过testRunner配置项指定使用jest-circus{ testRunner: jest-circus/runner }也可以使用 CLI 参数临时指定jest --testRunnerjest-circus/runner从源码看jest-circus/runner入口runner.ts实际导出的是 legacy-code-todo-rewrite/jestAdapter.ts 中的jestAdapter——它是 Jest 运行器与 Circus 之间的适配层负责初始化环境、注入beforeEach按配置执行resetModules/clearMocks/resetMocks/restoreMocks、加载setupFilesAfterEnv、加载测试文件、运行并转换结果最后把快照数据合并进TestResult见_addSnapshotData。五、事件流在源码中的具体实现5.1 声明阶段同步事件构建测试树当测试文件执行describe/it/beforeEach时index.ts 中的 API 只是把信息包装成同步事件派发出去// 以 add_test 为例 return dispatchSync({ asyncError, concurrent, failing: failing undefined ? false : failing, fn, mode, name: add_test, testName, timeout, });核心状态机 eventHandler.ts 的add_test分支会把测试挂到当前 describe 块下并处理各种非法声明测试嵌套在测试内部Cannot nest a describe inside a test/Tests cannot be nested测试在运行开始后才声明Tests must be defined synchronously在没有测试的 describe 块中使用 hookInvalid: beforeEach() may not be used in a describe block containing no tests.describe回调返回 Promise 或返回值Returning a Promise from describe is not supported。finish_describe_definition分支还会完成mode的向下传递describe.skip内的测试全部继承 skip与hasFocusedTests的标记存在test.only时置为 true。5.2 运行阶段run() 的执行顺序run.ts 的_runTestsForDescribeBlockOnce实现了标准的执行顺序派发run_describe_start执行该 describe 块的所有beforeAll若未 skip若开启了随机执行randomize配置用种子化的伪随机数生成器打乱子节点顺序shuffleArray按序处理子节点嵌套 describe 块递归运行普通测试执行beforeEach → 测试函数 → afterEach并发测试test.concurrent被regroupConcurrentChildren聚合成一个整体通过p-limit以maxConcurrency默认 5限流并发执行若配置了测试重试testRetries失败的测试在全部测试结束后再重跑执行该 describe 块的所有afterAll派发run_describe_finish。值得注意的实现细节afterAll失败不会改变单个测试的 pass/fail 状态run.ts 中test_done在 afterEach 之后立即派发afterAll的失败被计入全局unhandledErrorsbeforeAll失败则会被摊派到该 describe 块下的每个测试addErrorToEachTestUnderDescribe。5.3 describe.retry描述块级重试在较新版本中Circus 还支持describe.retry块级重试。run.ts中的_runTestsForDescribeBlock会检查state.describeRetryOptions中是否存在该 describe 块的重试配置numRetries、logErrorsBeforeRetry、waitBeforeRetry若存在则循环执行整个块直到无错误、重试次数耗尽或出现外部状态expect外部断言状态、进程级错误等不可重试情况并派发describe_retry事件。这也印证了事件清单中的describe_retry与test_retry两类重试事件的分工。六、实践编写一个可用的自定义环境事件处理器综合以上知识一个完整的自定义环境示例可以这样组织假设项目配置testEnvironment指向该文件或通过environmentOptions传入import type {Event, State} from jest-circus; import {TestEnvironment as NodeEnvironment} from jest-environment-node; type SlowTestInfo {fullName: string; duration: number}; class ObservingEnvironment extends NodeEnvironment { private slowTests: ArraySlowTestInfo []; async handleTestEvent(event: Event, state: State) { switch (event.name) { case run_start: { // 运行开始可以在此做初始化 this.slowTests []; break; } case test_start: { // 单测开始含 hook 阶段注意事件与状态数据不可修改 console.log([start] ${event.test.name}); break; } case test_fn_success: case test_fn_failure: { // 测试函数执行结果 break; } case test_done: { const {name, duration, errors} event.test; if (typeof duration number duration 1000) { this.slowTests.push({fullName: name, duration}); } // errors 非空即为失败 break; } case run_finish: { // 运行结束汇总慢测试 console.table(this.slowTests); break; } case teardown: { // 收尾恢复全局错误处理器等工作已由内部完成 break; } default: break; } } } export default ObservingEnvironment;使用约束提醒不要修改event/state它们被设计为只读视图除start_describe_definition、finish_describe_definition、add_hook、add_test、error五个同步事件外处理器返回的 Promise 都会被等待因此可以在处理器内执行异步工作但需注意不要影响测试本身的时序语义如需自定义全局事件处理器链而非仅环境内观察可以使用addEventHandler/removeEventHandler见 state.ts或通过EVENT_HANDLERSSymbol 注入处理器列表事件与状态类型定义集中在 packages/jest-types/src/Circus.ts这是编写处理器时的API 参考手册。七、进一步探索事件状态机核心实现packages/jest-circus/src/eventHandler.ts运行调度顺序、并发、重试packages/jest-circus/src/run.ts测试/Hook API 与事件派发packages/jest-circus/src/index.ts状态管理getState/setState/dispatchpackages/jest-circus/src/state.tsJest 适配层runner 入口packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts事件与状态类型定义packages/jest-types/src/Circus.ts事件处理相关的测试用例packages/jest-circus/src/tests/eventHandler.test.ts、packages/jest-circus/src/tests/run.test.ts、packages/jest-circus/src/tests/hooks.test.ts总结而言理解jest-circus的关键在于把握事件驱动 集中状态两条主线测试的生命周期被拆解为run_start → run_describe_start → hook_start → test_start → test_fn_start → … → test_done → run_finish → teardown的事件序列任何关注点报告、统计、诊断、自定义行为都可以通过handleTestEvent挂接到这条事件流上这正是它simple to extend的架构基础。【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考