如何使用Jest测试应用程序

如何使用Jest测试应用程序

软件测试对于确保应用程序按预期运行至关重要,尤其是在引入变更时。在开发早期捕捉并修复错误对于保持代码的弹性和高质量至关重要。

在众多可用的 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 testnpm testnpm t 。它将运行项目的 Jest。

执行测试后,结果就是这样:

string-format.test.js 的 Jest 测试结果成功

string-format.test.js 的 Jest 测试结果成功。

结果显示了一个测试套件(string-format.test.js 文件)、一个成功执行的测试("truncates a string correctly")以及在配置中定义的 displayName (Ecommerce)。

3. 在 string-format.js 中,如果添加一个额外的句点来破坏代码并运行测试,则会失败:

由于截断函数被破坏,Jest 测试结果失败

由于截断函数被破坏,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 测试结果显示了描述标签

成功的 Jest 测试结果显示了描述标签。

截图显示, describe 函数中的标签创建了一个块。尽管 describe 是可选项,但将测试分组到一个文件中并提供更多上下文信息还是很有帮助的。

在测试套件中组织测试

在 Jest 中,测试用例由 test 测试函数、 expect 期望函数和匹配器组成。相关测试用例的集合就是测试套件。在前面的示例中,string-format.test.js 就是一个测试套件,由一个测试用例组成,用于测试 string-format.js 文件。

假设项目中还有更多文件,如 file-operations.jsapi-logger.jsnumber-format.js。您可以为这些文件创建测试套件,如 file-operations.test.jsapi-logger.test.jsnumber-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 的 asyncawait 或 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 对象包含五个属性: userIdidtitlecompleteddescription

2. 执行测试:

显示异步代码测试失败的 Jest 测试结果

显示异步代码测试失败的 Jest 测试结果。

如截图所示, getTodos() 测试失败。它期望得到 description 属性,但 API 并没有返回它。有了这些信息,您就可以要求公司的 API 管理团队在应用程序需要时加入该属性,或者更新测试以满足 API 的响应。

3. 删除 description 属性的断言并重新运行测试:

Jest 测试结果显示异步代码测试通过

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 测试结果

使用模拟函数的成功 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 节点。

第一个 expectgetByTestId("submit-details") ,并使用 not 修饰符和 toBeDisabled 匹配器(从 react-testing-library 暴露)来断言按钮未被禁用。在每个匹配器中都使用 not 修饰符,以断言匹配器的对立面。

然后,代码会触发组件上的 submit 事件,并检查按钮是否禁用。你可以在测试库文档中找到更多自定义匹配器。

5. 现在,运行测试。如果克隆了 starter-files 分支,请在开始测试前运行 npm install ,确保安装了所有项目依赖项。

显示 react 组件测试通过的 Jest 测试结果

显示 react 组件测试通过的 Jest 测试结果。

运行代码覆盖率报告

Jest 还提供代码覆盖率报告,可显示项目的测试范围。

1. 向 Jest 传递 --coverage 选项。在 package.json 中(JavaScript 项目中)的 Jest 脚本中,使用此覆盖率选项更新 Jest 命令:

"scripts": {
"test": "jest --coverage"
}

2. 运行 npm run test 测试代码。你会得到如下报告:

每个测试服的成功 Jest 覆盖率报告

每个测试服的成功 Jest 覆盖率报告。

该报告显示,Jest 已 100% 测试了 SubmitButton.jsstring-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

以观察模式运行 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

使用 ghooks 在预提交期间运行 Jest。

小结

现在您知道如何将 Jest 集成到您的开发工作流中,以便在您进行更改时自动执行。这种方法可提供持续反馈,因此您可以在将更改发布到生产环境之前快速修复任何代码问题。

评论留言