完全指南:node/jsdom 内置环境、Docblock 按文件切换与自定义环境开发)
Jest 测试环境Test Environment完全指南node/jsdom 内置环境、Docblock 按文件切换与自定义环境开发【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jestJest 的测试环境Test Environment决定了测试代码运行在哪种全局上下文global context中是 Node 运行时还是浏览器模拟环境以及可用的全局 API如document、window。本文以 docs/TestEnvironment.md 为主线结合仓库内jest-environment-node、jest-environment-jsdom、jest-environment-jsdom-abstract等包的源码与 e2e 用例系统讲解内置环境的选型、testEnvironmentOptions配置、jest-environmentdocblock 的按文件切换以及如何通过继承内置环境或从零实现JestEnvironment接口来构建自定义环境读完即可在自己的项目中落地使用。一、什么是 Jest 测试环境testEnvironment是 Jest 的核心配置项之一用于指定测试代码运行的具体执行环境。Jest 通过它来决定测试文件中可以使用哪些全局对象例如process、document、window模块解析与require/import的行为测试代码运行所在的 VM 上下文Context。每个测试套件test suite都会运行在独立的TestEnvironment实例中setup和teardown每个套件只调用一次。从 packages/jest-runner/src/runTest.ts 可以看到每个测试文件运行时Jest 会以(globalConfig, projectConfig)和(console, docblockPragmas, testPath)两个对象分别构造环境实例const environment new TestEnvironment( { globalConfig, projectConfig, }, { console: testConsole, docblockPragmas, testPath: path, }, );随后在同一文件中environment.setup()在测试开始前被调用见 runTest.tsenvironment.teardown()在测试结束后被调用见 runTest.ts由此保证每个套件之间的全局状态相互隔离、互不污染。内置环境Jest 默认提供两种内置环境可通过testEnvironment配置项直接指定默认值为node环境名说明适用场景node默认环境提供 Node.js 运行时全局对象如process、global、Buffer等纯 Node 库、CLI 工具、后端逻辑等不依赖浏览器 API 的测试jsdom通过jsdom包模拟浏览器环境提供document、window等浏览器 API前端组件、DOM 操作、浏览器行为相关的测试二、在配置中指定环境testEnvironment 与 testEnvironmentOptionstestEnvironment在jest.config.js/jest.config.ts中通过testEnvironment指定整个项目的测试环境类型为node | jsdom | string默认node。相关完整说明见 docs/Configuration.md。const {defineConfig} require(jest); module.exports defineConfig({ testEnvironment: node, // 或 jsdom });import {defineConfig} from jest; export default defineConfig({ testEnvironment: jsdom, });testEnvironmentOptionstestEnvironmentOptions是传给测试环境的选项对象默认{}具体含义取决于当前使用的环境详见 docs/Configuration.md。Node 环境选项当使用node环境时可配置globalsCleanupon | soft | off默认soft控制测试之间全局变量的清理策略其余选项会传给vm.runInContextNode 官方 vm 模块的选项。globalsCleanup的解析逻辑在 packages/jest-environment-node/src/index.ts 的readGlobalsCleanupConfig中实现非on/soft/off的值会触发校验警告。JSDOM 环境选项当使用jsdom环境时可配置url页面 URL影响window.location与相对 URL 解析默认http://localhostuserAgentUser-Agent 字符串默认使用通用 UAhtml初始 HTML 内容默认!DOCTYPE html其余选项均透传给 jsdom。在 packages/jest-environment-jsdom-abstract/src/index.ts 中可以看到这些选项被合并进new JSDOM(...)的构造参数其中html、userAgent会被特殊处理其余选项通过展开运算符透传this.dom new JSDOM( typeof projectConfig.testEnvironmentOptions.html string ? projectConfig.testEnvironmentOptions.html : !DOCTYPE html, { pretendToBeVisual: true, resources: /* 根据 userAgent 创建 ResourceLoader */, runScripts: dangerously, url: http://localhost/, virtualConsole, ...projectConfig.testEnvironmentOptions, }, );示例自定义 jsdom 的初始 HTML、URL 与 UA对应 docs/Configuration.md 与 e2e 用例 e2e/custom-jsdom-html/package.jsonconst {defineConfig} require(jest); module.exports defineConfig({ testEnvironment: jsdom, testEnvironmentOptions: { html: html langen-US/html, url: https://jestjs.io/, userAgent: Agent/007, }, });import {defineConfig} from jest; export default defineConfig({ testEnvironment: jsdom, testEnvironmentOptions: { html: html langen-US/html, url: https://jestjs.io/, userAgent: Agent/007, }, });对应 e2e 测试 e2e/custom-jsdom-html/tests/test.js 通过document.querySelector(#root)验证自定义 HTML 已生效test(jsdom custom html, () { expect(document.querySelector(#root)).toBeTruthy(); });customExportConditionstestEnvironmentOptions.customExportConditions用于控制从package.json的exports字段加载哪个版本的库。内置环境的默认值为jest-environment-jsdom[browser]jest-environment-node[node, node-addons]这两个默认值分别在 packages/jest-environment-jsdom-abstract/src/index.ts 的customExportConditions [browser]与 packages/jest-environment-node/src/index.ts 的customExportConditions [node, node-addons]中定义且均支持通过testEnvironmentOptions.customExportConditions覆盖源码中会校验必须是字符串数组否则抛错。const {defineConfig} require(jest); module.exports defineConfig({ testEnvironment: jsdom, testEnvironmentOptions: { customExportConditions: [react-native], }, });三、为特定文件指定环境jest-environment docblock当testEnvironment配置作用于整个项目时如果只想让个别测试文件运行在不同环境中可以在文件顶部的注释docblock中通过jest-environment指令覆盖实现细粒度控制。使用内置环境在测试文件头部添加jest-environment注释后跟环境名称/** * jest-environment jsdom */ test(use jsdom in this test file, () { const element document.createElement(div); expect(element).not.toBeNull(); });/** * jest-environment jsdom */ test(use jsdom in this test file, () { const element document.createElement(div); expect(element).not.toBeNull(); });使用自定义环境jest-environment同样支持指向自定义环境的相对路径/** * jest-environment ./my-custom-environment.js */ test(use jsdom in this test file, () { const element document.createElement(div); expect(element).not.toBeNull(); });/** * jest-environment ./my-custom-environment.ts */ test(use jsdom in this test file, () { const element document.createElement(div); expect(element).not.toBeNull(); });结合 jest-environment-options 按文件覆盖环境选项docblock 中还可以用jest-environment-options传入 JSON 形式的选项与配置文件中的testEnvironmentOptions合并后生效对应 docs/Configuration.md 中的示例/** * jest-environment jsdom * jest-environment-options {url: https://jestjs.io/} *//** * jest-environment jsdom * jest-environment-options {url: https://example.com/, userAgent: Custom Agent} */该机制的底层实现在 packages/jest-runner/src/runTest.ts运行器读取docblockPragmas[jest-environment-options]若为字符串则JSON.parse后通过浅合并生成新的projectConfig.testEnvironmentOptions再传给环境构造函数。四、扩展内置环境继承 NodeEnvironment / JSDOMEnvironment如果内置环境已接近需求只是需要补充额外功能或微调行为推荐继承内置环境类来创建自定义环境。继承 NodeEnvironment以下示例继承jest-environment-node的NodeEnvironment对应 docs/TestEnvironment.md 中的完整代码// An example of a custom Node environment const NodeEnvironment require(jest-environment-node); /** * implements {import(jest-environment-node).NodeEnvironment} */ class CustomNodeEnvironment extends NodeEnvironment { constructor(config, context) { super(config, context); console.log(config.globalConfig); console.log(config.projectConfig); this.testPath context.testPath; this.docblockPragmas context.docblockPragmas; } async setup() { await super.setup(); await someSetupTasks(this.testPath); this.global.someGlobalObject createGlobalObject(); // Will trigger if docblock contains my-custom-pragma my-pragma-value if (this.docblockPragmas[my-custom-pragma] my-pragma-value) { // ... } } async teardown() { this.global.someGlobalObject destroyGlobalObject(); await someTeardownTasks(); await super.teardown(); } getVmContext() { return super.getVmContext(); } async handleTestEvent(event, state) { if (event.name test_start) { // ... } } } module.exports CustomNodeEnvironment;// An example of a custom Node environment import NodeEnvironment from jest-environment-node; export default class CustomNodeEnvironment extends NodeEnvironment { constructor(config, context) { super(config, context); console.log(config.globalConfig); console.log(config.projectConfig); this.testPath context.testPath; this.docblockPragmas context.docblockPragmas; } async setup() { await super.setup(); await someSetupTasks(this.testPath); this.global.someGlobalObject createGlobalObject(); // Will trigger if docblock contains my-custom-pragma my-pragma-value if (this.docblockPragmas[my-custom-pragma] my-pragma-value) { // ... } } async teardown() { this.global.someGlobalObject destroyGlobalObject(); await someTeardownTasks(); await super.teardown(); } getVmContext() { return super.getVmContext(); } async handleTestEvent(event, state) { if (event.name test_start) { // ... } } }然后在 Jest 配置中声明使用该环境const {defineConfig} require(jest); module.exports defineConfig({ testEnvironment: ./custom-node-environment.js, });import {defineConfig} from jest; export default defineConfig({ testEnvironment: ./custom-node-environment.ts, });环境生命周期与回调语义从源码角度NodeEnvironment的核心结构见 packages/jest-environment-node/src/index.ts包括global测试代码可访问的全局对象通过vm.createContext创建并注入 Node 全局、Buffer、ArrayBuffer、Uint8Array及installCommonGlobals安装的 Jest 公共全局moduleMockerjest-mock的ModuleMocker实例fakeTimers/fakeTimersModern旧版与新版 fake timerssetup()/teardown()环境生命周期钩子getVmContext()返回可供vm运行的上下文可选handleTestEvent(event, state)接收 jest-circus 的事件。需要注意的语义原文档中的 note测试文件中任何 docblock pragma如my-custom-pragma my-value都会作为context.docblockPragmas传给环境构造函数handleTestEvent是可选的。当它返回 Promise 时jest-circus 会等待其 settle 后再继续——但以下同步事件除外start_describe_definition、finish_describe_definition、add_hook、add_test、error。在仓库中环境实例的handleTestEvent通过 packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts 注册为 jest-circus 的事件处理器if (environment.handleTestEvent) { addEventHandler(environment.handleTestEvent.bind(environment)); }继承 JSDOMEnvironmentjest-environment-jsdom包的JSDOMEnvironment是内置 jsdom 环境的实现见 packages/jest-environment-jsdom/src/index.ts可直接继承扩展。另外Jest 还提供了jest/environment-jsdom-abstract包帮助你基于 jsdom 组合自定义环境或绑定你自己安装的 jsdom 版本对应 docs/TestEnvironment.md 的 tip 部分const JSDOMEnvironment require(jest/environment-jsdom-abstract); const jsdom require(jsdom); class CustomJSDOMEnvironment extends JSDOMEnvironment { constructor(config, context) { super(config, context, jsdom); } // Override methods to customize behavior } module.exports CustomJSDOMEnvironment;import JSDOMEnvironment from jest/environment-jsdom-abstract; import jsdom from jsdom; export default class CustomJSDOMEnvironment extends JSDOMEnvironment { constructor(config, context) { super(config, context, jsdom); } // Override methods to customize behavior }配置声明方式相同const {defineConfig} require(jest); module.exports defineConfig({ testEnvironment: ./custom-jsdom-environment.js, });import {defineConfig} from jest; export default defineConfig({ testEnvironment: ./custom-jsdom-environment.ts, });仓库中的 e2e 用例 e2e/custom-jsdom-version/v27/custom-jsdom-env.js 正是这一模式的实际应用——它继承jest/environment-jsdom-abstract的BaseEnv并传入项目自己安装的jsdom27import JSDOM from jsdom; import BaseEnv from jest/environment-jsdom-abstract; export default class JestJSDOMEnvironment extends BaseEnv { constructor(config, context) { super(config, context, JSDOM); } }其配套配置 e2e/custom-jsdom-version/v27/package.json 中声明了testEnvironment: ./custom-jsdom-env.js展示了如何绕开内置 jsdom 版本、使用自定义 jsdom 版本的完整方案。五、从零实现自定义环境JestEnvironment 接口当内置环境无法满足需求时可以创建自己的包或指定一个合法的 JS/TS 文件路径导出符合 Environment 形状的对象。最佳实践是以jest-environment-前缀命名自定义环境包例如jest-environment-foo使其易于识别为 Jest 环境。最小实现JestEnvironment接口定义在 packages/jest-environment/src/index.ts核心成员包括global、getVmContext()、setup()、teardown()以及可选的handleTestEvent、exportConditions等export declare class JestEnvironmentTimer unknown { constructor(config: JestEnvironmentConfig, context: EnvironmentContext); global: Global.Global; fakeTimers: LegacyFakeTimersTimer | null; fakeTimersModern: ModernFakeTimers | null; moduleMocker: ModuleMocker | null; getVmContext(): Context | null; setup(): Promisevoid; teardown(): Promisevoid; handleTestEvent?: Circus.EventHandler; exportConditions?: () Arraystring; }其中JestEnvironmentConfig包含projectConfig与globalConfig两个字段见 packages/jest-environment/src/index.tsEnvironmentContext包含console、docblockPragmas、testPath三个字段见 packages/jest-environment/src/index.ts。一个最小可用的自定义环境如下对应 docs/TestEnvironment.md/** * implements {import(jest/environment).JestEnvironment} */ class CustomEnvironment { // Implement the required methods here // Example of a method getVmContext() { return null; } } module.exports CustomEnvironment;import type {JestEnvironment} from jest/environment; export default class CustomEnvironment implements JestEnvironment { // Implement the required methods here // Example of a method getVmContext() { return null; } }配置声明const {defineConfig} require(jest); module.exports defineConfig({ testEnvironment: ./environment.js, });import {defineConfig} from jest; export default defineConfig({ testEnvironment: ./environment.ts, });关键约束从 Jest 27 起自定义环境必须导出getVmContext方法它是旧runScript的替代。若缺失运行器会直接报错退出见 packages/jest-runner/src/runTest.tsif (typeof environment.getVmContext ! function) { console.error( Test environment found at ${testEnvironment} does not export a getVmContext method, which is mandatory from Jest 27. ..., ); process.exit(1); }在自定义环境内可以访问config.projectConfig.testEnvironmentOptions包括customExportConditions等并通过exportConditions()方法向模块解析器暴露自定义的导出条件。环境实例在每次测试运行中由运行器负责setup()/teardown()的调度自定义实现中应保证setup()在测试前完成初始化、teardown()负责清理全局状态、释放 fake timers 等资源参考内置环境 packages/jest-environment-node/src/index.ts 的 teardown 实现。六、延伸阅读Configuration - testEnvironmentConfiguration - testEnvironmentOptions内置环境源码packages/jest-environment-node/src/index.ts、packages/jest-environment-jsdom/src/index.ts、packages/jest-environment-jsdom-abstract/src/index.ts环境接口定义packages/jest-environment/src/index.ts环境加载与生命周期调度packages/jest-runner/src/runTest.ts相关 e2e 用例e2e/custom-jsdom-html、e2e/custom-jsdom-version/v27【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考