如何使用 Jest 测试 JavaScript
测试是每个开发者都应该具备的一项重要技能。然而,有些开发者却不愿进行测试。我们都曾遇到过一些人,他们认为测试毫无用处,或者编写测试太费劲。虽然刚开始编写测试时可能会有这种感觉,但一旦你学会了如何正确地测试你的应用,你就再也不会后悔了。为什么?因为编写良好的测试能让你自信地交付健壮的应用。
测试至关重要
假设你正在开发一个全新的应用。你已经编码了几周甚至几个月,所以你已经精通代码,对它的每个部分都了如指掌。那么,为什么还要对你已经了解的东西编写测试呢?
嗯,代码库越大,维护起来就越困难。添加新功能时,总会有破坏代码的时候。这时,你就必须开始调试,修改现有代码,并祈祷你的修复不会破坏其他功能。如果真的破坏了,你就会想:“我受够了这个应用!哪怕只是发布一个小功能,我都会破坏一些东西!”
再举个例子。你找到一个没有测试的现有代码库。同样如此:祝你好运,添加新功能,不出现任何倒退!
但是,如果你正在与其他开发者合作呢?如果你除了修复应用之外别无选择,该怎么办?你将进入重启阶段:当你决定重新构建所有现有功能时,因为你不再确定到底发生了什么。
这两个示例的解决方案都是编写测试。现在看起来可能很浪费时间,但实际上以后会节省时间。以下是编写测试的一些主要好处:
- 您可以重构代码而不会破坏任何东西,因为测试可以告诉您是否发生了错误。
- 您可以自信地发布新功能,而不会出现任何倒退。
- 您的代码将变得更加文档化,因为我们可以看到测试的作用。您可以减少测试应用的时间,从而将更多时间投入到重要工作中。
所以,是的,编写测试需要时间。是的,一开始确实很难。是的,构建应用程序听起来更有趣。但我还是要再说一遍:编写测试至关重要,如果正确实施,可以节省时间。
在本文中,我们将发现一个为 JavaScript 应用程序编写测试的强大工具:Jest。
发现 Jest
简而言之,Jest 是 Facebook 打造的一款一体化 JavaScript 测试工具。为什么说是一体化呢?因为仅使用 Jest,你就能做到以下所有事情:
- 安全快速地运行测试
- 对你的代码做出断言
- 模拟函数和模块
- 添加代码覆盖率
- 快照测试
- 还有更多!
虽然你确实可以使用其他测试工具,例如Mocha、Chai或Sinon,但我更喜欢使用 Jest,因为它使用简单。
安装
要添加 Jest,只需在项目中添加一个包即可:
npm install --save-dev jest
然后您可以在文件test
中添加脚本package.json
:
{
"scripts": {
"test": "jest"
}
}
jest
默认运行将查找并运行位于__tests__
文件夹中或以.spec.js
或结尾的文件.test.js
。
测试文件的结构
Jest 提供了一些函数来构建你的测试:
describe
:用于对测试进行分组并描述函数/模块/类的行为。它接受两个参数。第一个参数是描述分组的字符串。第二个参数是一个回调函数,其中包含测试用例或钩子函数(更多信息请见下文😉)。it
或test
:这是你的测试用例,也就是你的单元测试。它必须具有描述性。参数与 完全相同describe
。beforeAll (afterAll)
:在所有测试之前(之后)运行的钩子函数。它接受一个参数:你将在所有测试之前(之后)运行的函数。beforeEach (afterEach)
:在每次测试之前(之后)运行的钩子函数。它接受一个参数:每次测试之前(之后)运行的函数。
注意:
beforeAll
、、beforeEach
和其他钩子函数之所以这样称呼,是因为它们允许您调用自己的代码并修改测试的行为。- 可以使用
.skip
ondescribe
andit
:it.skip(...)
或来跳过(忽略)测试describe.skip(...)
。 .only
您可以使用ondescribe
和it
:it.only(...)
或 来精确选择要运行的测试describe.only(...)
。如果您有很多测试,而只想关注一个测试,那么这很有用。
第一次测试
describe("My first test suite", () => {
it("adds two numbers", () => {
expect(add(2, 2)).toBe(4);
});
it("substracts two numbers", () => {
expect(substract(2, 2)).toBe(0);
});
});
匹配器
编写测试时,通常需要对代码进行断言。例如,如果用户在登录屏幕上输入了错误的密码,则屏幕上会显示错误信息。更一般地说,要进行断言,你需要一个输入和一个预期的输出。Jest 通过提供匹配器来测试我们的值,让我们可以轻松地做到这一点:
expect(input).matcher(output);
以下是最常见的一种:
toBe
:比较原始值(布尔值、数字、字符串)或对象和数组的引用(又称引用相等)
expect(1 + 1).toBe(2);
const firstName = "Thomas";
const lastName = "Lombart";
expect(`${firstName} ${lastName}`).toBe("Thomas Lombart");
const testsAreEssential = true;
expect(testsAreEssential).toBe(true);
toEqual
:递归比较数组或对象的所有属性(又称深度相等)。
const fruits = ["banana", "kiwi", "strawberry"];
const sameFruits = ["banana", "kiwi", "strawberry"];
expect(fruits).toEqual(sameFruits);
// Oops error! They don't have the same reference
expect(fruits).toBe(sameFruits);
const event = {
title: "My super event",
description: "Join me in this event!",
};
expect({ ...event, city: "London" }).toEqual({
title: "My super event",
description: "Join me in this event!",
city: "London",
});
toBeTruthy
(toBeFalsy
) :表示值是否为true
(false
)。
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
expect(false).toBeFalsy();
expect("Hello world").toBeTruthy();
expect({ foo: "bar" }).toBeTruthy();
not
:必须放在匹配器前面并返回匹配器结果的相反结果。
expect(null).not.toBeTruthy();
// same as expect(null).toBeFalsy()
expect([1]).not.toEqual([2]);
toContain
:检查数组是否包含参数中的元素
expect(["Apple", "Banana", "Strawberry"]).toContain("Apple");
toThrow
:检查函数是否抛出错误
function connect() {
throw new ConnectionError();
}
expect(connect).toThrow(ConnectionError);
它们并非唯一的匹配器,远非如此。你还可以在Jest 文档 toMatch
、、toBeGreaterThan
以及更多内容toBeUndefined
中探索!toHaveProperty
Jest CLI
我们介绍了测试文件的结构以及 Jest 提供的匹配器。让我们看看如何使用它的 CLI 来运行测试。
运行测试
让我们回顾一下在 Discover Jest 课程中看到的内容:仅运行jest
。默认情况下,jest
将查找目录的根目录并运行位于文件夹中或以或__tests__
结尾的所有文件。.spec.js
.test.js
您还可以指定要运行的测试文件的文件名或模式:
jest Event # run all test files containing Event
jest src/EventDetail.test.js # run a specific file
现在假设你想运行一个特定的测试,Jest 允许你使用以下-t
选项来实现。例如,考虑以下两个测试套件:
describe("calculator", () => {
it("adds two numbers", () => {
expect(2 + 2).toBe(4)
})
it("substracts two numbers", () => {
expect(2 - 2).toBe(0)
})
it("computes something", () => {
expect(2 * 2).toBe(4)
})
})
describe("example", () => {
it("does something", () => {
expect(foo()).toEqual("bar")
})
it("does another thing", () => {
const firstName = "John"
const lastName = "Doe"
expect(`${firstName} ${lastName}`).toBe("John Doe")
})
})
通过运行以下命令:
jest -t numbers
Jest 将运行前两个测试,calculator.test.js
但会跳过其余测试。
观看模式
然后是我认为 Jest 最方便的选项:watch mode
。此模式会监视文件更改并重新运行与之相关的测试。要运行它,你只需使用以下--watch
选项:
jest --watch
注意:Jest 借助 Git 知道哪些文件被修改了。因此,你必须在项目中启用 Git 才能使用该功能。
覆盖范围
让我们看看最后一个选项,它能让你了解 Jest 的强大之处:收集测试覆盖率,也就是测量测试套件运行时覆盖的代码量。这个指标有助于确保你的代码被测试正确覆盖。要使用这个指标,请运行以下命令:
jest --coverage
注意:追求所有组件的 100% 覆盖率毫无意义,尤其对于 UI 测试而言(因为变化很快)。对于最重要的组件,例如与支付相关的模块或组件,最好达到 100% 的覆盖率。
如果我向您展示 Jest CLI 提供的所有可能的选项,那么本文将花费您很长时间,因此如果您想了解有关它们的更多信息,请查看它们的文档。
模拟
Mock 是一个模拟真实对象行为的伪模块。换句话说,Mock 允许我们伪造代码,从而隔离正在测试的内容。
但是为什么测试中需要模拟呢?因为在实际应用中,你会依赖很多东西,比如数据库、第三方 API、库、其他组件等等。然而,你通常不想测试代码所依赖的东西,对吧?你可以放心地假设代码所依赖的东西运行良好。让我们举两个例子来说明模拟的重要性:
- 你想测试一个
TodoList
组件,它从服务器获取待办事项并显示出来。问题在于:你需要运行服务器来获取它们。如果这样做,你的测试会变得既缓慢又复杂。 - 你有一个按钮,点击后会从十张图片中随机选择一张。问题在于:你事先并不知道会选择哪张图片。你能做的最好的就是确保所选图片是十张图片中的一张。因此,你的测试需要具有确定性。你需要提前知道会发生什么。你猜对了,模拟测试可以做到这一点。
模拟函数
您可以使用以下函数轻松创建模拟:
jest.fn();
虽然看起来不像,但这个函数确实很强大。它有一个mock
属性,可以让我们跟踪函数被调用的次数、参数、返回值等等。
const foo = jest.fn();
foo();
foo("bar");
console.log("foo", foo); // foo ƒ (){return e.apply(this,arguments)}
console.log("foo mock property", foo.mock); // Object {calls: Array[2], instances: Array[2], invocationCallOrder: Array[2], results: Array[2]}
console.log("foo calls", foo.mock.calls); // [Array[0], Array[1]]
在这个例子中,你可以看到,由于foo
函数已被调用两次,所以calls
有两个项代表了两次函数调用中传递的参数。因此,我们可以对传递给函数的内容进行断言:
const foo = jest.fn();
foo("bar");
expect(foo.mock.calls[0][0]).toBe("bar");
编写这样的断言有点繁琐。幸运的是,Jest 提供了一些有用的匹配器,可以用来进行模拟断言,例如toHaveBeenCalled
:toHaveBeenCalledWith
toHaveBeenCalledTimes
const hello = jest.fn();
hello("world");
expect(hello).toHaveBeenCalledWith("world");
const foo = jest.fn();
foo("bar");
foo("hello");
expect(foo).toHaveBeenCalledTimes(2);
expect(foo).toHaveBeenNthCalledWith(1, "bar");
expect(foo).toHaveBeenNthCalledWith(2, "hello");
// OR
expect(foo).toHaveBeenLastCalledWith("hello");
让我们来看一个真实的例子:一个多步骤表单。每个步骤都有表单输入框以及两个按钮:上一步和下一步。点击“上一步”或“下一步”会触发一个saveStepData(nextOrPreviousFn)
函数,该函数会保存你的数据并执行nextOrPreviousFn
回调函数,将你重定向到上一步或下一步。
假设你想测试这个saveStepData
函数。如上所述,你不需要关心nextOrPreviousFn
它的实现。你只需要确保它在保存后被正确调用。那么你可以使用模拟函数来做到这一点。这种有用的技术称为依赖注入:
function saveStepData(nextOrPreviousFn) {
// Saving data...
nextOrPreviousFn();
}
const nextOrPreviousMock = jest.fn();
saveStepData(nextOrPreviousMock);
expect(nextOrPreviousMock).toHaveBeenCalled();
到目前为止,我们知道了如何创建模拟以及它们是否被调用。但是,如果我们需要更改函数的实现或修改返回值以使我们的测试之一具有确定性,该怎么办?我们可以使用以下函数来实现:
jest.fn().mockImplementation(implementation);
// Or with the shorthand
jest.fn(implementation);
我们马上尝试一下:
const foo = jest.fn().mockImplementation(() => "bar");
const bar = foo();
expect(foo.mock.results[0].value).toBe("bar");
// or
expect(foo).toHaveReturnedWith("bar");
// or
expect(bar).toBe("bar");
在这个例子中,你可以看到我们可以模拟函数的返回值foo
。因此,变量bar
保存的是"bar"
字符串。
注意:也可以使用mockResolvedValue
或模拟异步函数mockRejectedValue
来分别解决或拒绝Promise。
模拟模块
当然,我们可以模拟函数。但是你可能会想,模块怎么办?它们也很重要,因为我们几乎在每个组件中都会导入它们!别担心,Jest 已经帮你搞定了jest.mock
。
使用起来非常简单。只需输入要模拟的模块的路径,然后所有内容都会自动模拟。
例如,我们以最流行的 HTTP 客户端之一axios为例。事实上,你肯定不想在测试中执行实际的网络请求,因为它们可能会非常慢。axios
那么,让我们来模拟一下:
import axiosMock from "axios";
jest.mock("axios");
console.log(axiosMock);
注意:我给模块起了个名字axiosMock
,并非axios
为了方便阅读。我想明确说明这是一个模拟模块,而不是真正的模块。可读性越高越好!
现在有了诸如 、 等不同的函数,它们jest.mock
都被模拟了。因此,我们可以完全控制返回的内容:axios
get
post
axios
import axiosMock from "axios";
async function getUsers() {
try {
// this would typically be axios instead of axiosMock in your app
const response = await axiosMock.get("/users");
return response.data.users;
} catch (e) {
throw new Error("Oops. Something wrong happened");
}
}
jest.mock("axios");
const fakeUsers = ["John", "Emma", "Tom"];
axiosMock.get.mockResolvedValue({ data: { users: fakeUsers } });
test("gets the users", async () => {
const users = await getUsers();
expect(users).toEqual(fakeUsers);
});
Jest 的另一个很棒的功能是共享模拟。事实上,如果你要重用上面的 axios 模拟实现,你只需在文件夹__mocks__
旁边创建一个包含文件的文件夹即可:node_modules
axios.js
module.exports = {
get: () => {
return Promise.resolve({ data: { users: ["John", "Emma", "Tom"] } });
},
};
然后在测试中:
import axiosMock from "axios"
// Note that we still have to call jest.mock!
jest.mock("axios")
async function getUsers() {
try {
const response = await axios.get("/users")
return response.data.users
} catch (e) {
throw new Error("Oops. Something wrong happened")
}
}
test("gets the users", async () => {
const users = await getUsers()
expect(users.toEqual(["John", "Emma", "Tom"]))
}
配置 Jest
Jest 开箱即用,并非无法配置,远非如此!Jest 有很多配置选项。您可以通过三种不同的方式配置 Jest:
- 通过
jest
键入(如果你读过我上一篇文章,则与或键package.json
相同)eslintConfig
prettier
- 通过
jest.config.js
- 通过任何
json
或js
文件使用jest --config
。
大多数情况下,您会使用第一个和第二个。
让我们看看如何为 React 应用配置 Jest,特别是使用Create React App (CRA)
事实上,如果你不使用 CRA,你就必须自己编写配置。因为这部分内容与设置 React 应用( Babel、Webpack等)有关,所以我就不在这里介绍了。这里有一个直接来自 Jest 文档的链接,解释了如何在不使用 CRA 的情况下进行设置。
如果您正在使用 CRA,那么您无需执行任何操作,Jest 已经设置好了(尽管可以覆盖特定键的配置)。
然而,CRA 已经帮您设置好了 Jest,您也不必担心如何设置。因此,您将在下方找到一些常用的 Jest 配置键,这些键您可能会用到,或者将来会用到。您还将了解 CRA 如何使用它们。
匹配测试文件
你可以通过testMatch
key 指定一个全局模式来告诉 Jest 需要运行哪些测试。默认情况下,CRA 使用以下模式:
{
"testMatch": [
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
]
}
这种模式意味着 Jest 将对位于文件夹中或扩展名以或为前缀的.js
、jsx
和ts
文件运行测试。tsx
src
__tests__
spec
test
例如,这些测试文件将被匹配:
- ✅
src/example.spec.js
- ✅
src/__tests__/Login.jsx
- ✅
src/__tests__/calculator.ts
- ✅
src/another-example.test.js
但这些不会被匹配:
- ❌
src/Register.jsx
- ❌
src/__tests__/style.css
每次测试前设置
Jest 有一个名为 的键setupFilesAfterEnv
,它实际上是每次测试运行前要运行的文件列表。你可以在这里配置测试框架(例如React Testing Library或Enzyme),或者创建全局模拟。
CRA 默认将此文件命名为src/setupTests.js
。
配置测试覆盖率
正如 Jest CLI 课程中所述,您可以使用该--coverage
选项轻松查看代码覆盖率。您也可以对其进行配置。
假设您希望(或不希望)覆盖特定文件。您可以使用collectCoverageFrom
键来实现。例如,CRA 希望覆盖src
文件夹中 JavaScript 或 TypeScript 文件的代码,但不希望.d.ts
覆盖(typings)文件:
{
"collectCoverageFrom": ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"]
}
如果需要,您还可以使用键指定覆盖率阈值coverageThreshold
。在以下示例中,jest --coverage
如果分支、行、函数和语句覆盖率低于 75%,则运行将失败:
{
"coverageThreshold": {
"global": {
"branches": 75,
"functions": 75,
"lines": 75,
"statements": 75
}
}
}
转换
如果您使用 JavaScript 或 TypeScript 的最新功能,Jest 可能无法正确运行您的文件。在这种情况下,您需要在实际运行之前对其进行转换。为此,您可以使用transform
键,它将正则表达式映射到转换器路径。例如,CRA 使用babel-jest来处理 JS/TS 文件:
{
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
}
}
正如开头所说,Jest 有更多配置选项。好奇的话可以看看他们的文档!
文章来源:https://dev.to/thomaslombart/how-to-test-javascript-with-jest-3bkg