在专业厨房烘焙与在家烘焙完全不同。在家里,您不会总是拥有厨师货架上的所有独特食材。您可能不会拥有相同的高档电器或相同的无可挑剔的厨房。然而,这并不意味着你不能烤出美味的甜点。你只需要适应。
同样,在浏览器中运行 JavaScript 与在 Node.js 中运行 JavaScript 有很大不同。在某些情况下,在浏览器中运行的 JavaScript 代码根本无法在 Node.js 中运行,反之亦然。因此,为了测试您的前端应用程序,您必须经历一些额外的工作,但这并不意味着您不能这样做。通过一些修改,您可以使用 Node.js 来运行为浏览器编写的 JavaScript,就像 Louis 可以在家里烤令人垂涎的芝士蛋糕一样,无需他在面包店拥有的精美法式炊具。
在本节中,您将学习如何使用 Node.js 和 Jest 来测试编写在浏览器中运行的代码。
在浏览器中,JavaScript 可以访问不同的 API,因此具有不同的功能。
在浏览器中,JavaScript 可以访问一个名为 window 的全局变量。通过 window 对象,您可以更改页面的内容、触发用户浏览器中的操作并对事件(如点击和按键)做出反应。
例如,您可以通过 window 将侦听器附加到按钮上,这样每次用户单击它时,您的应用程序都会更新面包店库存中物品的数量。
尝试创建一个能够做到这一点的应用程序。 编写一个 HTML 文件,其中包含一个按钮和一个计数,并加载一个名为 main.js 的脚本,如下所示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Inventory Manager</title>
</head>
<body>
<h1>Cheesecakes: <span id="count">0</span></h1>
<button id="increment-button">Add cheesecake</button>
<script src="main.js"></script> ❶
</body>
</html>
❶ 用于使页面具有交互性的脚本
在 main.js 中,通过 ID 找到按钮,并为其附加一个侦听器。 每当用户单击此按钮时,都会触发侦听器,应用程序将增加芝士蛋糕的数量。
let data = { count: 0 };
const incrementCount = () => { ❶
data.cheesecakes++;
window.document.getElementById("count").innerHTML = data.cheesecakes;
};
const incrementButton = window.document.getElementById("increment-button");
incrementButton.addEventListener("click", incrementCount); ❷
❶ 更新应用程序状态的函数
❷ 附加一个事件监听器,它会在点击按钮时调用 incrementCount
要查看此页面的运行情况,请在 index.html 所在的文件夹中执行 npx http-server ./,然后访问 localhost:8080。
因为这个脚本运行在浏览器中,所以它可以访问窗口,因此它可以操作浏览器和页面中的元素,如图6.1所示。
与浏览器不同,Node.js 无法运行该脚本。 如果您尝试使用 node main.js 执行它,Node.js 会立即告诉您它发现了一个 ReferenceError,因为“窗口未定义”。
发生该错误是因为 Node.js 没有窗口。相反,因为它旨在运行不同类型的应用程序,所以它允许您访问 API,例如 process(包含有关当前 Node.js 进程的信息)和 require(允许您导入不同的 JavaScript 文件)。
现在,如果您要为 incrementCount 函数编写测试,则必须在浏览器中运行它们。因为您的脚本依赖于 DOM API,所以您将无法在 Node.js 中运行这些测试。如果您尝试这样做,您会遇到与执行 node main.js 时看到的相同的 ReferenceError。鉴于 Jest 依赖于 Node.js 特定的 API,因此只能在 Node.js 中运行,因此您也不能使用 Jest。
为了能够在 Jest 中运行您的测试,您可以使用 JSDOM 将浏览器 API 引入 Node.js,而不是在浏览器中运行您的测试。您可以将 JSDOM 视为可以在 Node.js 中运行的浏览器环境的实现。它使用纯 JavaScript 实现 Web 标准。例如,使用 JSDOM,您可以模拟操作 DOM 并将事件侦听器附加到元素。
JSDOM JSDOM 是一种完全用 JavaScript 编写的 Web 标准实现,您可以在 Node.js 中使用它。
要了解 JSDOM 是如何工作的,让我们使用它来创建一个表示 index.html 的对象,并且我们可以在 Node.js 中使用它。
首先,使用 npm init -y 创建一个 package.json 文件,然后使用 npm install --save jsdom 安装 JSDOM。
通过使用 fs,您将读取 index.html 文件并将其内容传递给 JSDOM,以便它可以创建该页面的表示。
const fs = require("fs");
const { JSDOM } = require("jsdom");
const html = fs.readFileSync("./index.html");
const page = new JSDOM(html);
module.exports = page;
页面表示包含您可以在浏览器中找到的属性,例如 window.properties。 因为您现在处理的是纯 JavaScript,所以您可以在 Node.js 中使用 page。
尝试在脚本中导入页面并像在浏览器中一样与之交互。 例如,您可以尝试在页面上附加一个新段落,如下所示。
const page = require("./page"); ❶
console.log("Initial page body:");
console.log(page.window.document.body.innerHTML);
const paragraph = page.window.document.createElement("p"); ❷
paragraph.innerHTML = "Look, I'm a new paragraph"; ❸
page.window.document.body.appendChild(paragraph); ❹
console.log("Final page body:");
console.log(page.window.document.body.innerHTML);
❶ 导入页面的 JSDOM 表示
❷ 创建段落元素
❸ 更新段落内容
❹ 将段落附加到页面上
要在 Node.js 中执行上一个脚本,请运行 node example.js。
使用 JSDOM,您几乎可以在浏览器中执行所有操作,包括更新 DOM 元素,例如计数。
const page = require("./page");
// ...
console.log("Initial contents of the count element:");
console.log(page.window.document.getElementById("count").innerHTML);
page.window.document.getElementById("count").innerHTML = 1337; ❶
console.log("Updated contents of the count element:");
console.log(page.window.document.getElementById("count").innerHTML);
// ...
❶ 更新计数元素的内容
感谢 JSDOM,您可以在 Jest 中运行您的测试,正如我所提到的,它只能在 Node.js 中运行。
通过将值“jsdom”传递给 Jest 的 testEnvironment 选项,你可以让它设置一个 JSDOM 的全局实例,你可以在运行测试时使用它。
要在 Jest 中设置 JSDOM 环境,如图 6.2 所示,首先创建一个名为 jest.config.js 的新 Jest 配置文件。 在此文件中,导出一个对象,其 testEnvironment 属性的值为“jsdom”。
jest.config.js
module.exports = {
testEnvironment: "jsdom",
};
注意 在撰写本文时,Jest 的当前版本是 26.6。 在此版本中,jsdom 是 Jest 的 testEnvironment 的默认值,因此您不一定需要指定它。
如果你不想手动创建 jest.config.js 文件,你可以使用 ./node_modules/.bin/jest --init 来自动化这个过程。 然后 Jest 的自动初始化会提示您选择一个测试环境并为您提供一个 jsdom 选项。
现在尝试创建一个 main.test.js 文件并导入 main.js 看看会发生什么。
main.test.js
require("./main");
如果您尝试使用 Jest 运行此测试,您仍然会收到错误消息。
FAIL ./main.test.js
● Test suite failed to run
TypeError: Cannot read property 'addEventListener' of null
10 |
11 | const incrementButton = window.document.getElementById("increment-button");
> 12 | incrementButton.addEventListener("click", incrementCount);
即使 window 现在存在,由于 Jest 设置了 JSDOM,它的 DOM 不是从 index.html 构建的。 相反,它是从一个空的 HTML 文档构建的,因此不存在increment-button按钮。 因为按钮不存在,所以不能调用它的 addEventListener 方法。
要使用 index.html 作为 JSDOM 实例将使用的页面,您需要在导入 main.js 之前读取 index.html 并将其内容分配给 window.document.body.innerHTML,如下所示。
const fs = require("fs");
window.document.body.innerHTML = fs.readFileSync("./index.html"); ❶
require("./main");
❶ 将 index.html 文件的内容分配给页面的正文
因为您现在已经将全局窗口配置为使用 index.html 的内容,所以 Jest 将能够成功执行 main.test.js。
为了能够为 incrementCount 编写测试,您需要采取的最后一步是公开它。 由于 main.js 不公开 incrementCount 或数据,因此您无法执行该函数或检查其结果。 通过使用 module.exports 导出数据和 incrementCount 函数来解决这个问题,如下所示。
// ...
module.exports = { incrementCount, data };
最后,您可以继续创建一个 main.test.js 文件,该文件设置初始计数、练习 incrementCount 并检查数据中的新计数。 同样,它是三个 As 模式——arrange、act、assert——就像我们之前所做的那样。
const fs = require("fs");
window.document.body.innerHTML = fs.readFileSync("./index.html");
const { incrementCount, data } = require("./main");
describe("incrementCount", () => {
test("incrementing the count", () => {
data.cheesecakes = 0; ❶
incrementCount(); ❷
expect(data.cheesecakes).toBe(1); ❸
});
});
❶排列:设置芝士蛋糕的初始数量
❷ Act:练习 incrementCount 函数,即被测单元
❸ Assert:检查data.cheesecakes 是否包含正确数量的cheesecakes
注意现在,我们不会担心检查页面的内容。 在接下来的部分中,您将学习如何在 DOM 上断言并处理由用户交互触发的事件。
一旦你庆祝看到这个测试通过,是时候解决最后一个问题了。
因为您已经使用 module.exports 来公开 incrementCount 和数据,所以 main.js 现在在浏览器中运行时会抛出错误。 要查看错误,请尝试使用 npx http-server ./ 再次为您的应用程序提供服务,并在浏览器的开发工具打开的情况下访问 localhost: 8080。
Uncaught ReferenceError: module is not defined
at main.js:14
您的浏览器会抛出此错误,因为它没有全局可用的模块。 同样,您遇到了与浏览器和 Node.js 之间差异相关的问题。
在使用 Node.js 模块系统的浏览器文件中运行的常见策略是使用一种工具将依赖项捆绑到浏览器可以执行的单个文件中。 Webpack 和 Browserify 等工具的主要目标之一就是进行这种捆绑。
将 browserify 作为开发依赖项安装,并运行 ./node_modules/.bin/browserify main.js -o bundle.js 将您的 main.js 文件转换为浏览器友好的 bundle.js。
注意您可以在 browserify.org 上找到 Browserify 的完整文档。
运行 Browserify 后,更新 index.html 以使用 bundle.js 而不是 main.js。
<!DOCTYPE html>
<html lang="en">
<!-- ... -->
<body>
<!-- ... -->
<script src="bundle.js"></script> ❶
</body>
</html>
❶ bundle.js 将从 main.js 生成。 它是一个包含 main.js 的所有直接和间接依赖项的单个文件。
提示每当 main.js 发生更改时,您都需要重新构建 bundle.js。
因为您必须经常运行它,所以最好创建一个 NPM 脚本,使用正确的参数运行 Browserify。
要创建运行 Browserify 的 NPM 脚本,请更新您的 package.json 以使其包含下一行。
{
// ...
"scripts": {
// ...
"build": "browserify main.js -o bundle.js" ❶
},
// ...
}
❶ 遍历 main.js 文件的依赖树,并将所有依赖项打包到一个 bundle.js 文件中
通过使用 Browserify 或 Webpack 等工具,您可以将编写的可测试代码转换为在 Node.js 中运行,以便它可以在浏览器中运行。
使用打包器可以让你单独测试你的模块,并且更容易在浏览器中管理它们。当您将应用程序捆绑到单个文件中时,您无需在 HTML 页面中管理多个脚本标签。
在本节中,您学习了如何使用 Node.js 和 Jest 来测试旨在在浏览器中运行的 JavaScript。您已经看到了这两个平台之间的差异,并了解了如何使用 JSDOM 将浏览器 API 引入 Node.js。
您还看到了 Browserify 如何通过将应用程序划分为单独的模块来帮助您测试应用程序,您可以在 Node.js 中测试这些模块,然后捆绑在浏览器中运行。
通过使用这些工具,您可以使用 Jest 在 Node.js 中测试您的浏览器应用程序。