Jest 测试环境(Test Environment)完全指南:node、jsdom 与自定义环境
2026/9/19 12:08:19 网站建设 项目流程

Jest 测试环境(Test Environment)完全指南:node、jsdom 与自定义环境

【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest

Jest 通过testEnvironment配置决定测试代码运行在何种宿主环境(Node.js 还是浏览器模拟环境 jsdom)中,并允许通过testEnvironmentOptions精细调节环境行为。本文以 Jest 官方文档 website/versioned_docs/version-30.4/TestEnvironment.md 为骨架,结合仓库内jest-environment-nodejest-environment-jsdomjest-environment-jsdom-abstract@jest/environment等包的源码实现,系统讲解内置环境的使用、单文件级别的 docblock 覆盖、内置环境扩展、从零编写自定义环境以及底层生命周期原理,帮助你在真实项目中按需选择与定制测试运行环境。

什么是测试环境

Jest 在运行测试文件时,会为每个测试套件创建一个独立的"沙箱"运行环境。测试代码中的global对象、浏览器 API(如documentwindow)、定时器与模块模拟等能力,都由这个环境提供。testEnvironment配置选项用于选择环境的类型,testEnvironmentOptions则用于向环境传递初始化参数。

Jest 默认内置两种开箱即用的环境:

  • node——默认环境。运行在 Node.js 运行时中,提供processBuffer等 Node 全局对象,适合测试后端逻辑、工具函数、模块系统等不依赖浏览器 API 的代码。
  • jsdom—— 通过 jsdom 包模拟浏览器环境,提供windowdocumentnavigator等 Browser API,适合测试涉及 DOM 操作、事件、localStorage等浏览器特性的前端代码。

从源码结构看,内置环境由独立包实现:node环境对应 packages/jest-environment-node,jsdom环境对应 packages/jest-environment-jsdom,二者都实现了 packages/jest-environment/src/index.ts 中定义的JestEnvironment接口。其中jsdom环境本身基于抽象的 packages/jest-environment-jsdom-abstract 实现,具体见 packages/jest-environment-jsdom/src/index.ts 中的JSDOMEnvironment类,它只做了一件事:把 JSDOM 模块传递给父类构造器:

import * as JSDOM from 'jsdom'; import BaseEnv from '@jest/environment-jsdom-abstract'; export default class JSDOMEnvironment extends BaseEnv { constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { super(config, context, JSDOM); } }

配置方式:全局生效与按文件覆盖

在 Jest 配置中全局设置

jest.config.js/jest.config.ts(或package.jsonjest字段)中设置testEnvironment,该值会作用于项目中的所有测试文件。完整说明见 docs/Configuration.md:

// jest.config.js const {defineConfig} = require('jest'); module.exports = defineConfig({ testEnvironment: 'node', // 默认值,可省略 });
// jest.config.ts import {defineConfig} from 'jest'; export default defineConfig({ testEnvironment: 'jsdom', // 切换到浏览器模拟环境 });

testEnvironment的取值可以是:

  • 内置环境名:'node''jsdom'
  • 指向自定义环境文件的路径(相对或绝对),如'./custom-node-environment.js'
  • 一个可解析的 npm 包名,如'jest-environment-puppeteer'

当使用非内置环境时,Jest 会尝试按文件路径或包名加载该模块,并期望其默认导出一个符合JestEnvironment形态的对象(见 docs/Configuration.md 第 2070 行附近的说明)。

使用 docblock pragma 按文件覆盖

全局配置粒度较粗,如果项目里大多数测试跑node,只有个别 DOM 相关的测试文件需要jsdom,可以在测试文件顶部的注释(docblock)中声明@jest-environmentpragma,实现单文件级别的覆盖:

/** * @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(); });

同样地,docblock 也可以指向自定义环境文件:

/** * @jest-environment ./my-custom-environment.js */ test('use custom environment in this test file', () => { const element = document.createElement('div'); expect(element).not.toBeNull(); });
/** * @jest-environment ./my-custom-environment.ts */ test('use custom environment in this test file', () => { const element = document.createElement('div'); expect(element).not.toBeNull(); });

在 docs/Configuration.md 中可以找到同样的用法示例,它与本指南中的示例完全一致,可以互相印证。

通过 docblock 传递环境选项

除了选择环境,docblock 还支持用@jest-environment-options传入 JSON 字符串形式的环境选项,字符串必须能被JSON.parse解析(见 docs/Configuration.md 中的说明):

/** * @jest-environment jsdom * @jest-environment-options {"url": "https://jestjs.io/"} */ test('use jsdom and set the URL in this test file', () => { expect(window.location.href).toBe('https://jestjs.io/'); });

多个选项可以合并:

/** * @jest-environment jsdom * @jest-environment-options {"url": "https://example.com/", "userAgent": "Custom Agent"} */ test('use custom URL and user agent', () => { expect(window.location.href).toBe('https://example.com/'); expect(window.navigator.userAgent).toContain('Custom Agent'); });

testEnvironmentOptions:环境初始化参数

testEnvironmentOptions默认值为{},会被整体传入环境的构造函数,具体支持哪些键取决于所用的环境(完整参考见 docs/Configuration.md)。

Node 环境选项

使用node环境时,可配置以下选项:

  • globalsCleanup'on'|'soft'|'off'):控制测试之间全局变量的清理策略,默认值为'soft'。该选项在 packages/jest-environment-node/src/index.ts 的readGlobalsCleanupConfig函数中被读取和校验,非法值会通过jest-validate输出警告;
  • 其余选项会直接透传给 Node.js 的vm.runInContext(见 Node.jsvm模块文档)。

在源码中可以看到,packages/jest-environment-node/src/index.ts 的构造函数会执行runInContext('this', Object.assign(this.context, projectConfig.testEnvironmentOptions)),即把testEnvironmentOptions合并进 vm 上下文后求值this作为global,这正是这些选项能被runInContext消费的实现依据。

JSDOM 环境选项

使用jsdom环境时,常用选项包括:

  • url—— 页面 URL,影响window.location和相对 URL 解析,默认值为"http://localhost"
  • userAgent—— user agent 字符串,默认是通用值;
  • html—— 初始 HTML 内容;
  • 其余选项透传给 jsdom 的JSDOM构造器(见 jsdom 文档)。

典型配置示例:

const {defineConfig} = require('jest'); module.exports = defineConfig({ testEnvironment: 'jsdom', testEnvironmentOptions: { html: '<html lang="en-US"></html>', url: 'https://jestjs.io/', userAgent: 'Agent/007', }, });
import {defineConfig} from 'jest'; export default defineConfig({ testEnvironment: 'jsdom', testEnvironmentOptions: { html: '<html lang="en-US"></html>', url: 'https://jestjs.io/', userAgent: 'Agent/007', }, });

这些参数的实际消费逻辑在 packages/jest-environment-jsdom-abstract/src/index.ts:构造器以projectConfig.testEnvironmentOptions.html作为JSDOM的第一个参数(默认'<!DOCTYPE html>'),并将userAgent交给ResourceLoader、其余选项整体展开传给JSDOM构造器,同时固定开启pretendToBeVisual: truerunScripts: 'dangerously',并用VirtualConsole把 jsdom 的 console 输出转发到context.console

customExportConditions:控制包的 exports 解析

testEnvironmentOptions还支持customExportConditions,用于控制从依赖包的package.jsonexports字段加载哪个版本。内置环境的默认值如下:

  • jest-environment-jsdom默认['browser']
  • jest-environment-node默认['node', 'node-addons']

例如为 React Native 场景覆盖 jsdom 的导出条件:

const {defineConfig} = require('jest'); module.exports = defineConfig({ testEnvironment: 'jsdom', testEnvironmentOptions: { customExportConditions: ['react-native'], }, });

在源码中,两个环境分别把默认值定义在类的customExportConditions字段上(packages/jest-environment-node/src/index.ts 为['node', 'node-addons'],packages/jest-environment-jsdom-abstract/src/index.ts 为['browser']),若用户在testEnvironmentOptions中提供了customExportConditions,则通过exportConditions()方法返回用户配置,否则返回默认值;非字符串数组的非法配置会直接抛出错误。

扩展内置环境(推荐做法)

Jest 允许继承内置的NodeEnvironmentJSDOMEnvironment来定制行为,例如在测试前注入全局对象、读取 docblock pragma 做条件逻辑、监听 circus 测试事件等。这是扩展环境的首选方式——无需从零实现全部接口。

继承 NodeEnvironment 的完整示例

// 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', });

环境生命周期与关键接口的源码印证

上面示例中出现的constructor(config, context)setup()teardown()getVmContext()handleTestEvent()等接口,正是 packages/jest-environment/src/index.ts 中JestEnvironment基类声明的契约。结合内置实现可以理解每个环节的真实行为:

  • setup():默认空实现。在 packages/jest-environment-node/src/index.ts 中只是空方法,扩展时在其中执行环境初始化逻辑;
  • teardown():负责释放资源。Node 环境会dispose两套 fake timers、清空context、并通过内部GlobalProxy.clear()删除测试期间设置到全局对象上的属性,防止内存泄漏(packages/jest-environment-node/src/index.ts);jsdom 环境则额外移除error事件监听并调用this.global.close()(packages/jest-environment-jsdom-abstract/src/index.ts);
  • getVmContext():返回测试代码运行的 vm 上下文。Node 环境返回this.context,jsdom 环境返回this.dom.getInternalVMContext()
  • handleTestEvent():可选实现。在 jest-circus 运行器中,如果环境提供了该方法,会被注册为事件处理器:见 packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts 中的addEventHandler(environment.handleTestEvent.bind(environment))

关于 handleTestEvent 的同步事件说明

文档特别提醒:handleTestEvent是可选方法;当它返回 Promise 时,jest-circus 会等待其 settle 之后再继续——但以下同步事件除外

  • start_describe_definition
  • finish_describe_definition
  • add_hook
  • add_test
  • error

这 5 个事件不会等待返回的 Promise,因此在处理这些事件时应避免依赖异步操作的结果。

关于 docblock pragmas 的传递

任何测试文件中的 docblock pragma(例如@my-custom-pragma my-value)都会以context.docblockPragmas的形式传入环境构造函数。在 packages/jest-environment/src/index.ts 中可以看到EnvironmentContext的完整定义,包含consoledocblockPragmastestPath三个字段,这也解释了为什么上面的示例能通过context.testPath拿到当前测试文件路径、通过context.docblockPragmas['my-custom-pragma']读取自定义 pragma。

基于 jsdom 的组合式扩展

Jest 还提供 packages/jest-environment-jsdom-abstract 包,方便你基于 jsdom 组合出自定义环境,或使用你自己安装的 jsdom 版本:

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', });

这个抽象包的设计动机从源码可见:BaseJSDOMEnvironment的受保护构造器接收第三个参数jsdomModule(packages/jest-environment-jsdom-abstract/src/index.ts),内部从该模块解构出JSDOMResourceLoaderVirtualConsole再组装环境。官方内置的jest-environment-jsdom正是用同样的方式把自己捆绑的 jsdom 传进去的,因此社区包完全可以依赖这个抽象层并注入自己的 jsdom 版本。

从零编写自定义环境(Custom Environment)

当继承内置环境仍无法满足需求时,可以创建自己的环境包或指定一个合法的 JS/TS 文件路径。该文件应导出一个符合JestEnvironment形态的对象,也就是实现 packages/jest-environment/src/index.ts 中声明的接口:构造函数接收(config, context),至少提供globalsetup()teardown()getVmContext()等成员。

/** * @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', });

更完整的自定义环境骨架可以参考 docs/Configuration.md 中给出的版本,其中构造函数接收config并解构出projectConfig使用。

命名规范

如果以 npm 包形式发布自定义环境,官方建议使用jest-environment-前缀命名(例如jest-environment-puppeteer),这样能被清晰地识别为 Jest 环境包。从加载机制上看,testEnvironment配置既支持文件路径也支持包名,命名规范能让你的包在生态中被其他开发者一眼认出。

生命周期与隔离保证

文档中有一条重要提示:每个测试套件运行在独立的TestEnvironment实例中,setupteardown每个套件只调用一次。这意味着:

  • 环境是按"测试文件(套件)"为单位创建的,而不是每个test()用例一个;
  • setup()中注入的全局对象、在构造函数中读取的配置,会在整个套件期间保持;
  • teardown()只在套件结束时执行一次,用于释放资源、清理全局对象。

这一设计也解释了为何环境类需要fakeTimersfakeTimersModernmoduleMocker等实例字段:它们都随环境实例化,为每个套件提供独立的定时器模拟与模块模拟状态,避免跨套件相互污染。

总结

  • 默认node、浏览器模拟选jsdom,通过testEnvironment全局配置,或通过@jest-environmentdocblock 按文件覆盖;
  • 环境行为通过testEnvironmentOptions微调:Node 环境透传vm.runInContext选项并支持globalsCleanup,jsdom 环境透传 jsdom 构造选项并支持url/userAgent/html等,二者都支持customExportConditions
  • 需要定制时优先继承NodeEnvironment/JSDOMEnvironment,或在@jest/environment-jsdom-abstract基础上注入自己的 jsdom 版本;极端场景再实现完整的JestEnvironment接口;
  • 每个测试套件拥有独立的TestEnvironment实例,setup/teardown每套件一次,环境通过handleTestEvent可接入 jest-circus 事件流。

相关参考资料:docs/Configuration.md 中的testEnvironmenttestEnvironmentOptions章节、packages/jest-environment/src/index.ts(环境接口定义)、packages/jest-environment-node/src/index.ts 与 packages/jest-environment-jsdom-abstract/src/index.ts(内置实现)、packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts(环境事件接入点)。

【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询