软件测试对于确保应用程序按预期运行至关重要,尤其是在引入变更时。在开发早期捕捉并修复错误对于保持代码的弹性和高质量至关重要。
在众多可用的 JavaScript 测试工具和框架中,Jest 是最受欢迎的工具和框架之一。作为 Meta 的产品,Jest 为 JavaScript 应用程序和使用 JavaScript 框架构建的应用程序提供了广泛的测试功能。
让我们来探讨一下 Jest 框架、它的功能以及如何最好地将它集成到您的开发工作流程中。
什么是 Jest?
Jest 是一个灵活易用的框架。除了核心的 JavaScript 测试功能外,它还提供配置和插件,以支持测试 Babel、webpack、Vite、Parcel 或基于 TypeScript 的应用程序。
Jest 已被开发人员广泛采用,并拥有一系列社区构建和维护的插件。它的突出特点是易于使用: JavaScript 测试不需要额外的配置或插件。但您也可以使用一些额外的配置选项执行更高级的测试,如测试 JavaScript 框架。
如何为 JavaScript 项目设置 Jest
让我们来探讨如何在现有 JavaScript 项目中设置 Jest。
前提条件
要学习本教程,请确保您已具备以下条件:
安装 Jest 软件包
1. 如果您还没有跟进本教程的项目,请将此仓库作为起点。
starter-files
分支为您提供了一个基础,以便在学习本教程的过程中构建应用程序。参考主分支查看本教程的代码,并交叉检查你的代码。
2. 要使用 npm 安装 Jest,请在终端中进入项目目录并运行以下命令:
npm install --save-dev jest
--save-dev
选项会告诉npm将软件包安装到 devDependencies
下,其中包含开发所需的依赖项。
配置 Jest
在 package.json 中配置 Jest
在 package.json 文件中,添加一个名为 jest
的对象,其属性如下所示:
{ … "jest": { "displayName": "Ecommerce", "globals": { "PROJECT_NAME": "Ecommerce TD" }, "bail": 20, "verbose": true }, }
测试期间,Jest 会搜索该对象并应用这些配置。您可以在 Jest 的配置页面查看其他选项,但该对象的属性包括
displayName
— Jest 将此属性值作为标签添加到测试结果中。globals
— 保存一个对象值,用于定义测试环境中可用的全局变量。bail
— 默认情况下,Jest 会运行所有测试并在结果中显示错误。bail
会告诉 Jest 在设定的失败次数后停止运行。verbose
— 设置为true
时,会在测试执行过程中显示单个测试报告。
在配置文件中配置 Jest
您还可以在 jest.config.js 文件中配置 Jest。Jest 还支持 .ts、.mjs、.cjs 和 .json 扩展名。执行测试时,Jest 会查找这些文件,并应用找到的文件中的设置。
例如,请看这个 jest.config.js 文件:
const config = { displayName: "Ecommerce", globals: { "PROJECT_NAME": "Ecommerce TD" }, bail: 20, verbose: true } module.exports = config;
该代码导出了一个 Jest 配置对象,其属性与上一示例相同。
你也可以使用一个包含可序列化 JSON 配置对象的自定义文件,并在执行测试时将文件路径传递给 --config
选项。
创建基本测试文件
配置好 Jest 后,就可以创建测试文件了。Jest 会审查项目的测试文件,执行它们并提供结果。测试文件通常采用 [name].test.js 或 [name]-test.js 等格式。这种模式便于 Jest 和你的团队识别测试文件。
请看包含以下代码的 string-format.js 文件:
function truncate( str, count, withEllipsis = true ) { if (str.length < = count) return str const substring = str.substr(0, count) if (!withEllipsis) return substring return substring + '...' } module.exports = { truncate }
函数 truncate()
将字符串截断到特定长度,并可选择添加省略号。
编写测试
1. 创建一个名为 string-format.test.js 的测试文件。
2. 为使文件井然有序,请将 string-format.test.js 文件放置在与 string-format.js 文件相同的目录下,或放置在特定的测试目录下。无论测试文件在项目的哪个位置,Jest 都能找到并执行它。使用 Jest,您可以在各种情况下测试应用程序。
3. 在 string-format.test.js 中编写一个基本测试,如下所示:
const { truncate } = require('./string-format') test('truncates a string correctly', () = > { expect(truncate("I am going home", 6)).toBe('I am g...') })
测试用例描述正确截断了一个字符串 truncates a string correctly
。这段代码使用了 Jest 提供的 expect
函数,该函数用于测试值是否与预期结果相匹配。
代码将 truncate("I am going home", 6)
作为参数传递给 expect
。这段代码将测试使用参数 "I am going home"
和 6
调用 truncate
所返回的值。 expect
调用会返回一个 expectation 对象,它提供了对 Jest 匹配的访问。
它还包含以 "I am g..."
为参数的 toBe
匹配器。 toBe
匹配器测试期望值和实际值是否相等。
执行测试
要执行测试,请定义 jest
命令。
1. 在项目的 package.json 文件中,添加此 test
脚本:
"scripts": { "test": "jest" }
2. 现在在终端运行 npm run test
、 npm test
或 npm t
。它将运行项目的 Jest。
执行测试后,结果就是这样:
string-format.test.js 的 Jest 测试结果成功。
结果显示了一个测试套件(string-format.test.js 文件)、一个成功执行的测试("truncates a string correctly"
)以及在配置中定义的 displayName
(Ecommerce
)。
3. 在 string-format.js 中,如果添加一个额外的句点来破坏代码并运行测试,则会失败:
由于截断函数被破坏,Jest 测试结果失败。
这一结果表明你破坏了 truncate
函数或进行了更新,需要更新测试。
如何使用 Jest 编写测试
Jest 测试语法
Jest 的专有语法简单易用。Jest 为您的项目提供全局方法和对象,用于编写测试。它的一些基本术语包括: describe
(描述)、 test
(测试)、 expect
(期望)和 matchers(匹配器)。
describe
: 该函数将相关测试组合到一个文件中。test
: 该函数用于运行测试。这是it
的别名。它包含要测试的值的断言。expect
: 该函数为各种值声明断言。它为各种形式的断言提供了匹配器。- Matchers: 它们可以让你以各种方式断言值。你可以断言值相等、布尔相等和上下文相等(如数组是否包含该值)。
要使用匹配器,请参考下面的示例:
1. 用以下代码替换 string-format.test.js 文件中的测试:
describe("all string formats work as expected", () = > { test("truncates a string correctly", () = > { expect( truncate("I am going home", 6) ).toBe("I am g...") }) })
2. 运行代码。
结果如下:
成功的 Jest 测试结果显示了描述标签。
截图显示, describe
函数中的标签创建了一个块。尽管 describe
是可选项,但将测试分组到一个文件中并提供更多上下文信息还是很有帮助的。
在测试套件中组织测试
在 Jest 中,测试用例由 test
测试函数、 expect
期望函数和匹配器组成。相关测试用例的集合就是测试套件。在前面的示例中,string-format.test.js 就是一个测试套件,由一个测试用例组成,用于测试 string-format.js 文件。
假设项目中还有更多文件,如 file-operations.js、api-logger.js 和 number-format.js。您可以为这些文件创建测试套件,如 file-operations.test.js、api-logger.test.js 和 number-format.test.js。
使用 Jest 匹配器编写简单断言
我们已经探讨了使用 toBe
匹配器的示例。使用其他 Jest 匹配器的断言包括:
toEqual
— 用于测试对象实例中的 “深度” 等价性。toBeTruthy
— 用于测试一个布尔值是否为真。toBeFalsy
— 用于测试一个布尔值是否为假。toContain
— 用于测试数组是否包含一个值。toThrow
— 用于测试调用的函数是否抛出错误。stringContaining
— 用于测试字符串是否包含子串。
让我们来看看使用其中一些匹配器的示例。
例如,您可能希望函数或代码返回一个具有特定属性和值的对象。
1. 请使用下面的代码片段测试这一功能。在这种情况下,您需要断言返回的对象等于预期对象。
expect({ name: "Joe", age: 40 }).toBe({ name: "Joe", age: 40 })
本例使用 toBe
。测试失败的原因是这个匹配器没有检查深度相等–它检查的是值,而不是所有属性。
2. 使用 toEqual
匹配器检查深度相等:
expect({ name: "Joe", age: 40 }).toEqual({ name: "Joe", age: 40 })
这个测试通过了,因为两个对象都 “深度相等”,也就是说它们的所有属性都相等。
3. 试试另一个匹配器示例,测试定义的数组是否包含特定元素。
expect(["orange", "pear", "apple"]).toContain("mango")
这个测试失败的原因是 toContain
断言 [ "orange"
、 "pear"
、 "apple"
] 数组包含预期值 "mango"
,但数组并不包含。
4. 使用变量进行与下面代码相同的测试:
const fruits = ["orange", "pear", "apple"]; const expectedFruit = "mango"; expect(fruits).toContain(expectedFruit)
测试异步代码
到目前为止,我们已经测试了同步代码–在代码执行下一行之前返回值的表达式。您也可以使用 Jest 的 async
、 await
或 Promises 来处理异步代码。
例如,apis.js 文件中有一个用于发出 API 请求的函数:
function getTodos() { return fetch('https://jsonplaceholder.typicode.com/todos/1') }
getTodos
函数向 https://jsonplaceholder.typicode.com/todos/1
发送 GET
请求。
1. 用以下代码创建名为 apis.test.js 的文件,以测试伪造的 API:
const { getTodos } = require('./apis') test("gets a todo object with the right properties", () = > { return getTodos() .then((response) = > { return response.json() }) .then((data) = > { expect(data).toHaveProperty('userId') expect(data).toHaveProperty('id') expect(data).toHaveProperty('title') expect(data).toHaveProperty('completed') expect(data).toHaveProperty('description') }) })
该测试用例调用了 getTodos
函数,以获取一个 todo
对象。解析 Promise 时,会使用 .then
方法获取解析后的值。
在该值中,代码返回 response.json()
,这是另一个将响应转换为 JSON 格式的 Promise。另一个 .then
方法会获取包含 expect
和匹配器的 JSON 对象。代码断言 JSON 对象包含五个属性: userId
、 id
、 title
、 completed
和 description
。
2. 执行测试:
显示异步代码测试失败的 Jest 测试结果。
如截图所示, getTodos()
测试失败。它期望得到 description
属性,但 API 并没有返回它。有了这些信息,您就可以要求公司的 API 管理团队在应用程序需要时加入该属性,或者更新测试以满足 API 的响应。
3. 删除 description
属性的断言并重新运行测试:
Jest 测试结果显示异步代码测试通过。
截图显示一切都通过了测试。
4. 现在尝试使用 async/await
代替传统的 Promise 处理:
test("gets a todo object with the right properties", async () = > { const response = await getTodos() const data = await response.json() expect(data).toHaveProperty("userId") expect(data).toHaveProperty("id") expect(data).toHaveProperty("title") expect(data).toHaveProperty("completed") })
现在, async
关键字位于函数之前。代码在 getTodos()
之前使用了 await
,在 response.json()
之前使用了 await
。
高级 Jest 功能
模拟函数和模块
在编写测试时,您可能希望测试一个具有外部依赖性的表达式。在某些情况下,尤其是单元测试,您的单元测试应与外部效应隔离。在这种情况下,您可以用 Jest 模拟您的函数或模块,以便更好地控制测试。
1. 例如,请看包含以下代码的 functions.js 文件:
function multipleCalls(count, callback) { if (count < 0) return; for (let counter = 1; counter <= count; counter++) { callback() } }
multipleCalls
函数根据 count
的值执行。它取决于回调函数–外部依赖关系。其目的是了解 multipleCalls
是否正确执行了外部依赖关系。
2. 要在测试文件 functions.test.js 中模拟外部依赖关系并跟踪依赖关系的状态,请使用以下代码:
const { multipleCalls } = require('./functions') test("functions are called multiple times correctly", () => { const mockFunction = jest.fn() multipleCalls(5, mockFunction) expect( mockFunction.mock.calls.length ).toBe(5) })
在这里, jest
对象的 fn
方法创建了一个模拟函数。然后,代码将 5
和 mock 函数作为参数传递,执行 multipleCalls
。然后,断言 mockFunction
被调用了 5 次。 mock
属性包含代码如何调用函数和返回值的信息。
3. 运行测试时,这就是预期结果:
使用模拟函数的成功 Jest 测试结果。
如图所示,代码调用了五次 mockFunction
。
在代码中,mock 函数模仿了外部依赖关系。当应用程序在生产中使用 multipleCalls
时,外部依赖关系是什么并不重要。单元测试并不关心外部依赖关系是如何工作的。它只是验证 multipleCalls
是否按预期运行。
4. 要模拟模块,请使用 mock
方法并传递一个文件路径,即模块:
const { truncate, } = require("./string-format") jest.mock("./string-format.js")
这段代码模仿 string-format.js 输出的所有函数,并跟踪其调用频率。模块的 truncate
变成了一个 mock 函数,这会导致函数失去其原始逻辑。你可以从 truncate.mock.calls.length
属性中了解测试中 truncate
的执行次数。
如果出现错误或代码无法运行,请将代码与完整的实现进行比较。
使用 Jest 和 React 测试库测试 React 组件
如果您还没有项目来跟进本教程,可以使用 React 示例项目作为起点。 starter-files
分支可以帮助您在学习本教程的过程中开始编写代码。将主分支作为参考,对照本教程的完整代码检查您的代码。
您可以使用 Jest 测试 React 等 JavaScript 框架。当你使用 Create React App 创建 React 项目时,它们会立即支持 React 测试库和 Jest。如果不使用 Create React App 创建 React 项目,则需要安装 Jest,以便使用 Babel 和 React 测试库测试 React。如果克隆 starter-app
分支,则无需安装依赖项或应用配置。
1. 如果使用示例项目,请使用此命令安装所需的依赖项:
npm install --save-dev babel-jest @babel/preset-env @babel/preset-react react-testing-library
您也可以使用 Enzyme 代替 React Testing Library。
2. 更新 babel.config.js 中的 Babel 配置,如果该文件不存在,则创建该文件:
module.exports = { presets: [ '@babel/preset-env', ['@babel/preset-react', {runtime: 'automatic'}], ], };
3. 请看包含以下代码的 src/SubmitButton.js 文件:
import React, { useState } from 'react' export default function SubmitButton(props) { const {id, label, onSubmit} = props const [isLoading, setisLoading] = useState(false) const submit = () => { setisLoading(true) onSubmit() } return
该 SubmitButton
组件接收三个道具:
id
— 按钮的标识符。label
— 要在按钮中显示的文本。onSubmit
— 当有人点击按钮时触发的函数。
代码将 id
属性分配给 data-testid
属性,该属性用于标识测试元素。
该组件还会跟踪 isLoading
状态,并在有人点击按钮时将其更新为 true
。
4. 为该组件创建测试。将以下代码放入 SubmitButton.test.js 文件中:
import {fireEvent, render, screen} from "@testing-library/react" import "@testing-library/jest-dom" import SubmitButton from "./SubmitButton" test("SubmitButton becomes disabled after click", () => { const submitMock = jest.fn() render( <SubmitButton id="submit-details" label="Submit" onSubmit={submitMock} / > ) expect(screen.getByTestId("submit-details")).not.toBeDisabled() fireEvent.submit(screen.getByTestId("submit-details")) expect(screen.getByTestId("submit-details")).toBeDisabled() })
上面的代码渲染了 SubmitButton
组件,并使用 screen.getByTestId
查询方法通过 data-testid
属性获取 DOM 节点。
第一个 expect
是 getByTestId("submit-details")
,并使用 not
修饰符和 toBeDisabled
匹配器(从 react-testing-library
暴露)来断言按钮未被禁用。在每个匹配器中都使用 not 修饰符,以断言匹配器的对立面。
然后,代码会触发组件上的 submit
事件,并检查按钮是否禁用。你可以在测试库文档中找到更多自定义匹配器。
5. 现在,运行测试。如果克隆了 starter-files
分支,请在开始测试前运行 npm install
,确保安装了所有项目依赖项。
显示 react 组件测试通过的 Jest 测试结果。
运行代码覆盖率报告
Jest 还提供代码覆盖率报告,可显示项目的测试范围。
1. 向 Jest 传递 --coverage
选项。在 package.json 中(JavaScript 项目中)的 Jest 脚本中,使用此覆盖率选项更新 Jest 命令:
"scripts": { "test": "jest --coverage" }
2. 运行 npm run test
测试代码。你会得到如下报告:
每个测试服的成功 Jest 覆盖率报告。
该报告显示,Jest 已 100% 测试了 SubmitButton.js 和 string-format.js 中的函数。它还显示 Jest 未测试 string-format.js 中的任何语句和行。测试覆盖率显示,string-format.js 中未覆盖的行是第 7 行和第 12 行。
在第 7 行, truncate
函数中的 return str
没有执行,因为 if (str.length <= count)
条件返回 false
。
在第 12 行,同样是在 truncate
函数中,return substring
不执行,因为 if (!withEllipsis)
条件返回 false
。
将 Jest 与您的开发工作流程相结合
让我们看看如何集成这些测试来改进开发工作流程。
在观察模式下运行测试
你可以在更改代码时使用观察模式自动运行测试,而不是手动执行测试。
1. 要启用观察模式,请更新 package.json(JavaScript 项目)中的 Jest 命令脚本,添加 --watchAll
选项:
"scripts": { "test": "jest --coverage --watchAll" }
2. 运行 npm run test
。它将在观察模式下触发 Jest:
以观察模式运行 Jest
每次更改项目时都会运行测试。这种方法有助于在构建应用程序时获得持续反馈。
设置预提交钩子
在 Git 环境中,只要发生特定事件(如拉、推或提交),钩子就会执行脚本。预提交钩子定义了哪些脚本会在预提交事件(代码在提交前触发的事件)中运行。
只有脚本不出错,提交才会成功。
在预提交前运行 Jest 可确保在提交前没有任何测试失败。
您可以使用各种库在项目中设置 git 挂钩,例如 ghooks。
1. 在 devDependencies
下安装 ghooks
:
npm install ghooks --save-dev
2. 在 package.json 文件(JavaScript 项目)的顶层添加 configs
对象。
3. 在 configs
下添加一个 ghooks
对象。
4. 添加一个关键字为 pre-commit
、值为 jest
的属性。
{ … "config": { "ghooks": { "pre-commit": "jest" } }, }
5. 提交代码 代码会触发预提交钩子,执行 Jest:
使用 ghooks 在预提交期间运行 Jest。
小结
现在您知道如何将 Jest 集成到您的开发工作流中,以便在您进行更改时自动执行。这种方法可提供持续反馈,因此您可以在将更改发布到生产环境之前快速修复任何代码问题。
评论留言