使用 Jest 测试 jQuery DOM 操作:从 JSDOM 环境搭建到异步回调 Mock 实战
使用 Jest 测试 jQuery DOM 操作:从 JSDOM 环境搭建到异步回调 Mock 实战
发布时间:2026/9/19 11:03:28
使用 Jest 测试 jQuery DOM 操作从 JSDOM 环境搭建到异步回调 Mock 实战【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest导读在 Jest 生态中直接操作 DOM 的代码尤其是基于 jQuery 的事件绑定、DOM 内容更新逻辑常被认为是最难测试的一类代码。本文以 Jest 仓库内置的 examples/jquery 示例为主线完整讲解如何借助jest-environment-jsdom模拟浏览器 DOM并结合jest.mock拦截异步网络请求在毫秒级内完成对「点击按钮 → 异步获取用户数据 → 更新页面文本」整条链路的单元测试。读完本文你将掌握 JSDOM 测试环境的配置方法、副作用模块的测试技巧以及如何分别测试「消费数据的视图层」与「发起请求的数据层」。测试目标一段典型的 jQuery DOM 操作代码文档给出的被测代码位于 examples/jquery/displayUser.js它描述了一个非常典型的浏览器端交互场景use strict; const $ require(jquery); const fetchCurrentUser require(./fetchCurrentUser.js); $(#button).click(() { fetchCurrentUser(user { const loggedText Logged (user.loggedIn ? In : Out); $(#username).text(user.fullName - loggedText); }); });这段代码暴露了三个测试难点模块加载即产生副作用require(./displayUser.js)的瞬间就会给#button注册click事件监听器测试必须保证 DOM 在模块加载前已经就绪事件触发异步回调点击后依赖fetchCurrentUser的异步回调来更新 DOM测试需要可控地触发回调真实的网络请求fetchCurrentUser内部执行$.ajax请求远程接口若不拦截会让测试变慢且不可重复。而仓库中对应的数据层实现 examples/jquery/fetchCurrentUser.js 进一步揭示了网络请求的来源const $ require(jquery); function parseJSON(user) { return { fullName: ${user.firstName} ${user.lastName}, loggedIn: true, }; } function fetchCurrentUser(callback) { return $.ajax({ success: user callback(parseJSON(user)), type: GET, url: http://example.com/currentUser, }); } module.exports fetchCurrentUser;可以看到fetchCurrentUser通过$.ajax发起GET http://example.com/currentUser请求成功后经parseJSON把{firstName, lastName}归一化为视图层需要的{fullName, loggedIn}结构。核心思路一用jest.mock拦截网络请求Jest 官方建议的做法是从根上消除网络请求直接 mock 掉fetchCurrentUser模块让测试不再发起真实的网络请求而是解析本地 mock 数据。正如原文档所强调的这能确保测试在毫秒级完成而不是数秒从而保障单元测试的快速迭代。这一点在 examples/jquery/tests/display_user.test.js 中体现得最为直接use strict; jest.mock(../fetchCurrentUser.js);jest.mock会自动把../fetchCurrentUser.js替换为自动生成的 mock 函数jest.fn()该模块内部真实存在的$.ajax请求自然就不会执行了。核心思路二用 JSDOM 模拟浏览器 DOM 环境视图层测试之所以能直接读写document、调用$(#button).click()是因为 Jest 的jsdom 测试环境。原文档明确指出jsdom与jest-environment-jsdom包会模拟出一个「仿佛置身浏览器」的 DOM 环境——你在测试中观察到的每一个 DOM API 行为与浏览器中的表现完全一致。在 Jest 30 的默认配置中测试环境默认是node见 docs/Configuration.md 中testEnvironment的默认值说明因此需要显式切换到 jsdom。示例项目 examples/jquery/package.json 中正是通过配置项完成的{ private: true, version: 0.0.0, name: example-jquery, devDependencies: { babel/core: ^7.27.4, babel/preset-env: ^7.27.2, babel-jest: workspace:*, jest: workspace:*, jest-environment-jsdom: workspace:* }, dependencies: { jquery: ^4.0.0 }, scripts: { test: jest }, jest: { testEnvironment: jsdom } }如果项目中没有安装jest-environment-jsdom需要先安装原文档给出的命令npm install --save-dev jest-environment-jsdom除了在package.json或jest.config.js中全局配置testEnvironment: jsdom也可以只对单个测试文件启用 jsdom——在文件顶部添加jest-environmentdocblock 注释即可这是 docs/Configuration.md 中提供的官方用法/** * jest-environment jsdom */ test(use jsdom in this test file, () { const element document.createElement(div); expect(element).not.toBeNull(); });从源码层面看jsdom 环境的载体是 packages/jest-environment-jsdom/src/index.ts它定义了JSDOMEnvironment类继承自jest/environment-jsdom-abstract的BaseEnv并将真实的jsdom库作为底层 DOM 实现注入从而为测试提供document、window等浏览器全局对象。完整测试拆解逐行理解displayUser-test下面是被测代码对应的完整测试文件与仓库中的 examples/jquery/tests/display_user.test.js 一致原文档中的displayUser-test.js与之对应use strict; jest.mock(../fetchCurrentUser); test(displays a user after a click, () { // Set up our document body document.body.innerHTML div span idusername / button idbutton / /div; // This module has a side-effect require(../displayUser); const $ require(jquery); const fetchCurrentUser require(../fetchCurrentUser); // Tell the fetchCurrentUser mock function to automatically invoke // its callback with some data fetchCurrentUser.mockImplementation(cb { cb({ fullName: Johnny Cash, loggedIn: true, }); }); // Use jquery to emulate a click on our button $(#button).click(); // Assert that the fetchCurrentUser function was called, and that the // #username spans inner text was updated as wed expect it to. expect(fetchCurrentUser).toHaveBeenCalled(); expect($(#username).text()).toBe(Johnny Cash - Logged In); });整个测试按「准备 DOM → 加载副作用模块 → 控制异步回调 → 模拟用户点击 → 断言行为」五步展开第 1 步手动构建 DOM 骨架document.body.innerHTML div span idusername / button idbutton / /div;displayUser.js的副作用会在模块加载时为#button绑定click事件因此测试必须先构造出包含#button与#username的 DOM 结构否则选择器将匹配不到任何元素、事件绑定也会静默失败。这正是原文档强调「我们需要为测试正确地设置 DOM」的原因。第 2 步通过require触发副作用require(../displayUser);注意这里用的是require而不是import且没有接收返回值——因为测试关心的不是该模块导出了什么而是加载它这个动作本身触发的 jQuery 事件绑定。注释// This module has a side-effect精确点明了这种「为副作用而加载」的测试模式。第 3 步让 mock 自动调用回调fetchCurrentUser.mockImplementation(cb { cb({ fullName: Johnny Cash, loggedIn: true, }); });mockImplementation会替换 mock 函数的实现当视图层调用fetchCurrentUser(user ...)时mock 立即用预设数据{fullName: Johnny Cash, loggedIn: true}调用传入的回调。这一步把异步网络行为同步化让测试可以确定性地驱动后续 DOM 更新逻辑。第 4 步用 jQuery 模拟真实点击$(#button).click();在 jsdom 环境下$(#button).click()会真实地触发绑定在#button上的 click 事件进而执行displayUser.js中注册的回调——这与用户在浏览器中点击按钮的行为路径完全一致。第 5 步双重断言expect(fetchCurrentUser).toHaveBeenCalled(); expect($(#username).text()).toBe(Johnny Cash - Logged In);第一个断言验证交互链路点击事件确实触发了fetchCurrentUser的调用第二个断言验证渲染结果#username的文本被更新为Johnny Cash - Logged InLogged (true ? In : Out)即为Logged In。两个断言一个管「请求发没发」一个管「界面改没改」共同锁定了这段 DOM 操作代码的行为契约。纵深拓展直接测试fetchCurrentUser数据层原文档聚焦于视图层测试而仓库中的 examples/jquery/tests/fetch_current_user.test.js 展示了互补的另一面——不 mock 业务模块而是 mock 掉 jQuery 本身直接验证数据层的请求参数与回调行为jest.mock(jquery); beforeEach(() jest.resetModules()); it(calls into $.ajax with the correct params, () { const $ require(jquery); const fetchCurrentUser require(../fetchCurrentUser); // Call into the function we want to test const dummyCallback () {}; fetchCurrentUser(dummyCallback); // Now make sure that $.ajax was properly called during the previous // 2 lines expect($.ajax).toHaveBeenCalledWith({ success: expect.any(Function), type: GET, url: http://example.com/currentUser, }); }); it(calls the callback when $.ajax requests are finished, () { const $ require(jquery); const fetchCurrentUser require(../fetchCurrentUser); // Create a mock function for our callback const callback jest.fn(); fetchCurrentUser(callback); // Now we emulate the process by which $.ajax would execute its own // callback $.ajax.mock.calls[0 /*first call*/][0 /*first argument*/].success({ firstName: Bobby, lastName: Marley, }); // And finally we assert that this emulated call by $.ajax incurred a // call back into the mock function we provided as a callback expect(callback.mock.calls[0 /*first call*/][0 /*first arg*/]).toEqual({ fullName: Bobby Marley, loggedIn: true, }); });这段测试有三个值得学习的技巧jest.mock(jquery)把整个 jQuery 替换为 mock 对象$.ajax自动变成jest.fn()从而无需真实网络即可验证请求参数beforeEach(() jest.resetModules())每个用例前重置模块注册表保证两次测试各自require(../fetchCurrentUser)时拿到的是全新、未被污染的模块实例避免 mock 状态跨用例串扰手工驱动$.ajax回调通过$.ajax.mock.calls[0][0].success(...)取出 mock 调用中传入的配置对象并手动调用其success回调等价于模拟服务器返回数据最终断言parseJSON的正确性{firstName: Bobby, lastName: Marley}被转换为{fullName: Bobby Marley, loggedIn: true}。这两层测试互为补充视图层测试站在「页面行为」视角数据层测试站在「请求契约」视角合起来构成了对 DOM 网络交互逻辑的完整覆盖。运行与验证在 examples/jquery 目录下直接运行npm test项目脚本test: jest定义于 examples/jquery/package.json测试文件位于 examples/jquery/tests下的display_user.test.js与fetch_current_user.test.js。运行成功后两个测试文件共 3 个用例1 个视图层 2 个数据层全部通过即可完整复现本文描述的测试方案。小结通过本文的实战拆解可以总结出测试 jQuery DOM 操作代码的三条核心方法论环境先行安装jest-environment-jsdom并通过testEnvironment: jsdom全局配置或jest-environment jsdom单文件配置切换测试环境获得与浏览器一致的 DOM API 行为按层隔离视图层测试用jest.mock拦截业务模块fetchCurrentUser数据层测试用jest.mock(jquery)拦截底层库让每一层都能独立、确定性地被验证手动驱动异步借助mockImplementation同步化异步回调、用mock.calls手工触发$.ajax的成功回调把「网络时序」转化为「确定的函数调用」从而保证测试在毫秒级完成且结果可复现。这套模式不仅适用于 jQuery同样可以迁移到任何「事件监听 异步数据 DOM 渲染」的前端代码测试场景。【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考