使用 Jest 测试 JavaScript - 单元测试
介绍
过去几周,我一直沉浸在测试我的 JavaScript 和 Python 项目的世界里,我的天哪!这太有趣了,我真不敢相信自己怎么没早点开始学。
我逐渐意识到,测试代码对于编写可维护、可复用且模块化的代码至关重要。它还能让任何贡献者、同事以及与我们合作的其他人几乎完全确信他们的新代码coolAndGreatFunction420()
不会破坏我们的整个项目。
本文将介绍测试的工作原理、用途以及如何使用 jest 在我们的 Node.js 应用程序中实现它。
什么是测试?
测试代码是确保软件按照预期运行的过程。测试代码可以帮助我们对最终产品更加放心。
例如,如果我们有一个程序,其目的是将 2 + 2 相加并返回 4,我们希望确保它确实能做到这一点。我们不希望它返回 5、1 或“cuatro”,我们希望它返回 4。测试使我们能够确保该程序每次运行时都能按预期运行。
测试软件的形式和规模各不相同。例如,我们可以通过简单地像用户一样使用上述程序来测试它。我们可以启动终端、浏览器或任何类型的 GUI,并多次运行该程序,确保它始终返回预期值。然而,最有趣的测试是自动化测试。
自动化测试就是用代码来测试代码。是不是很棒?这可以通过使用允许我们编写测试代码的框架来实现。
虽然本文主要关注自动化测试,但我认为手动测试程序仍然很重要。这样才能确保最终用户获得最佳的产品体验。
需要注意的是,无论测试多么深入或复杂,都无法确保代码无缺陷。然而,我坚信测试能够提升代码质量,最终打造出更优质的产品。
测试类型
在进入实际示例之前,我们应该了解常见的测试类型。这些并非唯一存在的类型,但却是 JavaScript 世界中最流行的类型。
单元测试
单元测试涵盖代码块,确保它们按照预期的方式运行。单元可以是一个函数、一个类或整个模块。我个人建议单元测试仅限于函数,因为我会尝试先测试代码中最小的部分,但这并没有真正的规则。我们可以有两种类型的单元:
-
孤立或单独的单元:没有其他依赖关系的单元,其行为和/或输出仅取决于其中包含的块。
-
社交单元:这些单元具有依赖关系。它们的执行和可选输出依赖于其他单元。在测试时,这意味着我们必须确保它们的依赖关系能够按预期工作,然后再进行测试。
// This is an isolated unit
function myNameIs(nameString) {
return `Will the real ${nameString} please stand up`;
};
// This is a sociable unit, because it depends on other units
function pleaseStandUp() {
return myNameIs("Slim Shady") + "please stand up, please stand up";
};
集成测试
单元测试通过并不意味着我们的应用程序功能齐全、功能完备。一旦我们确定所有单元都经过了适当的测试,并且能够独立运行,我们就会按照它们在软件中的使用方式对它们进行集成测试。这就是集成测试。将这些单元组合在一起并进行测试,可以确保我们的函数、类和模块能够良好地协同工作。
端到端测试(E2E)
端到端测试(E2E)会对我们的应用程序进行从头到尾的全面测试。我的意思是,这种测试关注的是用户使用软件时的体验。
还记得我说过,即使我们已经设置了自动化测试,手动测试仍然很重要吗?嗯,端到端测试本质上就是自动化的手动测试(试着向非开发人员解释一下)。这些测试通常在无头浏览器中进行,尽管它们也可以在带有图形用户界面的浏览器中运行。通过我们的测试,我们尝试尽可能多地复制用户与我们网站的交互,并确保输出符合我们的预期。
除了复制用户在网站上的导航流程之外,我实际上还喜欢尝试在这些类型的测试中打破常规,就好像我是一个在网站上疯狂打字和点击的用户一样。
使用 Jest 进行单元测试
Jest是 Facebook 的一款开源产品,它使我们能够在几乎任何我们喜欢的 JavaScript 框架中编写和运行测试。
📖 注意
我知道,我不喜欢 Jest —— 以及 React —— 是 Facebook 的产品。然而,我必须承认它们是好产品,而 Facebook 的主题就另当别论了。
要在我们的项目中安装和使用 Jest,我们可以运行:
$ npm i -D jest
然后我们可以添加一个测试脚本到我们的package.json
:
"scripts": {
"test": "jest"
}
每当运行 Jest 时,它都会自动查找并运行以 结尾的文件.test.js
,.spec.js
或目录.js
内的任何文件__tests__
。
现在,让我们开始编写要测试的单元。别担心,这些代码可能看起来很简单,但它们是我在实际项目中用到的实际函数。
// helpers.js
function isNumber(possibleNumber) {
return typeof possibleNumber === "number";
};
module.exports = isNumber;
好了,一个非常简单的函数,应该不难测试……对吧?让我们尝试编写第一个测试。在本例中,我们假设测试文件与 helpers.js 模块位于同一目录中。
// helpers.test.js
const isNumber = require("./helpers");
test("should return true if type of object is a number", () => {
expect(isNumber(5)).toBe(true);
});
这就是一个基本的 Jest 文件的样子。我们导入想要测试的模块/类/函数,指定一些我们期望的测试结果的描述,然后我们告诉 Jest 我们预期的函数结果是什么。让我们稍微分解一下。
-
test()
是一个 Jest 函数,它定义要运行的单个测试。您可以test
在一个文件中包含任意数量的语句。它需要两个必需参数和一个可选的第三个参数。第一个参数是测试名称。通常使用它来清楚地描述所要测试的内容。第二个参数是一个函数,其中包含我们的测试主体。我们在这里告诉 Jest 我们对测试的期望。在这种情况下,我们期望的返回值为。第三个参数是一个可选值,以毫秒为isNumber(5)
单位。由于测试通常非常快,我们预计任何单个测试都不会花费超过 5 秒的时间,这是默认值。true
timeout
timeout
-
expect()
是我们用来实际测试期望值的函数。我们将其expect
与“匹配器”函数一起使用,该函数会断言某个值的特定条件。在这个测试中,我们使用了toBe()
匹配器,它将实际值与我们的期望值进行比较。匹配器有很多,我在这里只介绍其中几个,但您可以在其文档的 Jest 匹配器部分中阅读更多相关信息。
📖 注意
测试中可以包含
expect()
任意数量的语句,没有限制!建议您只使用合理数量的语句,因为您可以编写test()
任意数量的语句,并且将语句expect()
与其各自的测试用例组织在一起会更具可读性。
现在我们已经编写了第一个测试,我们可以运行npm run test
并看到奇迹发生:
$ npm run test
> testing-javascript-with-jest@1.0.0 test
> jest
PASS ./helpers.test.js
✓ should return true if type of object is a number (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.279 s, estimated 1 s
Ran all test suites.
📖 注意
“测试套件”是指 jest 运行的文件数量,而“测试”是指
test()
在这些文件中找到的所有语句。
正如我之前所说,Jest 会自动查找并运行源代码中的所有测试文件,而且速度非常快。恭喜你编写了你的第一个单元测试!
让我们为此功能编写更多测试,以确保我们涵盖尽可能多的用例。
// helpers.test.js
const isNumber = require("./helpers");
test("should return true if type of object is a number", () => {
expect(isNumber(0)).toBe(true);
expect(isNumber(5)).toBe(true);
expect(isNumber(+"5")).toBe(true);
});
test("should return false if type of object is not a number", () => {
expect(isNumber(null)).toBe(false);
expect(isNumber("number")).toBe(false);
expect(isNumber(undefined)).toBe(false);
});
我们npm run test
再次奔跑并且……
$ npm run test
...
PASS ./helpers.test.js
✓ should return true if type of object is a number (2 ms)
✓ should return false if type of object is not a number
...
太棒了!我们的函数似乎按预期运行了。
分组测试describe()
我们可以像刚才那样,只在顶层编写测试。然而,我们可以看到,尽管可以看到测试描述及其结果,但我们无法通过终端输出判断测试的是哪个单元。为了更好地说明这一点,我们再编写一个函数,helpers.js
并将其相应的测试添加到 中helpers.test.js
。
// helpers.js
...
function isObject(possibleObject) {
return typeof possibleObject === "object";
};
module.exports = { isNumber, isObject };
// helpers.test.js
const { isNumber, isObject } = require("./helpers");
...
test('should return true if type of object is "object"', () => {
expect(isObject({})).toBe(true);
expect(isObject([])).toBe(true);
});
test('should return false if type of object is not "object"', () => {
expect(isObject(5)).toBe(false);
expect(isObject("object")).toBe(false);
});
我们npm run test
再次运行并获得了预期的(哈,明白了吗?)结果:
$ npm run test
> testing-javascript-with-jest@1.0.0 test
> jest
PASS ./helpers.test.js
✓ should return true if type of object is a number (1 ms)
✓ should return false if type of object is not a number (1 ms)
✓ should return true if type of object is "object" (1 ms)
✓ should return false if type of object is not "object" (1 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 0.204 s, estimated 1 s
Ran all test suites.
正如我之前所说,虽然这些结果很棒,而且我们得到了所有绿色的勾选标记,但它们的可读性并不好,而且我们不知道哪个测试属于哪个单元。有一种更好的方法来组织我们的测试,以便终端的输出更清晰、更易读。
通过使用describe()
,我们可以将测试分组到一个代码块下,从而将它们置于相同的作用域下——这在以后会很有用。要describe()
在现有测试中实现该功能,我们只需用describe()
语句包裹一组相关的test()
语句即可。
// helpers.test.js
...
describe("isNumber", () => {
test("should return true if type of object is a number", () => {
expect(isNumber(0)).toBe(true);
expect(isNumber(5)).toBe(true);
expect(isNumber(+"5")).toBe(true);
});
test("should return false if type of object is not a number", () => {
expect(isNumber(null)).toBe(false);
expect(isNumber("number")).toBe(false);
expect(isNumber(undefined)).toBe(false);
});
});
describe("isObject", () => {
test('should return true if type of object is "object"', () => {
expect(isObject({})).toBe(true);
expect(isObject([])).toBe(true);
});
test('should return false if type of object is not "object"', () => {
expect(isObject(5)).toBe(false);
expect(isObject("object")).toBe(false);
});
});
这一次,当我们运行时npm run test
,我们将看到以相同名称组织的测试组。
$ npm run test
...
PASS ./helpers.test.js
isNumber
✓ should return true if type of object is a number (2 ms)
✓ should return false if type of object is not a number (1 ms)
isObject
✓ should return true if type of object is "object" (1 ms)
✓ should return false if type of object is not "object" (1 ms)
当将测试分组在一起时,终端输出和编写的代码都会变得更具可读性,并且出于在未来的文章中会变得重要的原因,它还将相关测试分组在同一范围内。
使用 Jest Each 运行多个测试用例
从 Jest 23 版本开始,我们已经能够each
在test
和describe
函数上使用 该方法。允许我们使用“表列”中定义的值多次运行相同的测试。使用Spock Data Tableseach
时,表可以是数组类型,也可以是模板字面量。
我们可以使用包含不同值的多个语句来简化测试,expect
如下所示:
//helpers.test.js
...
describe("isNumber", () => {
// Instead of this:
// test("should return true if type of object is a number", () => {
// expect(isNumber(0)).toBe(true);
// expect(isNumber(5)).toBe(true);
// expect(isNumber(+"5")).toBe(true);
// });
// We use this:
const numbers = [0, 5, +"5"];
test.each(numbers)("should return true since type of %j is a number",
numberToTest => {
expect(isNumber(numberToTest)).toBe(true);
});
我知道这语法有点奇怪,但它能让我们用更少的代码轻松测试大量测试。在这种情况下,我们可以继续向numbers
数组中添加值,并检查它们是否都返回,true
而无需添加额外的expect()
语句。
📖 注意
%j 是一个
printf formatting specifier
。当我们运行 jest 时,每个测试都会有一个唯一的名称,其特定值将通过参数传递给它的字符串注入table
。
让我们对所有测试都这样做:
// helpers.test.js
...
describe("isNumber", () => {
const numbers = [0, 5, +"5"];
const notNumbers = [null, "number", undefined];
test.each(numbers)('should return true since type of %j is "number"',
possibleNumber => {
expect(isNumber(possibleNumber)).toBe(true);
});
test.each(notNumbers)('should return false since type of %j is not "number"',
possibleNumber => {
expect(isNumber(possibleNumber)).toBe(false);
});
});
describe("isObject", () => {
const objects = [{}, []];
const notObjects = [5, "object"];
test.each(objects)('should return true since type of %j is "object"',
possibleObject => {
expect(isObject(possibleObject)).toBe(true);
expect(isObject(possibleObject)).toBe(true);
});
test.each(notObjects)('should return false since type of %j is not "object"',
possibleObject => {
expect(isObject(possibleObject)).toBe(false);
expect(isObject(possibleObject)).toBe(false);
});
});
现在,我们不仅节省了不必要的代码行,而且当打印到终端时,我们的测试都有唯一的名称:
$ npm run test
...
PASS ./helpers.test.js
isNumber
✓ should return true since type of 0 is "number" (1 ms)
✓ should return true since type of 5 is "number"
✓ should return true since type of 5 is "number"
✓ should return false since type of null is not "number" (1 ms)
✓ should return false since type of "number" is not "number"
✓ should return false since type of undefined is not "number"
isObject
✓ should return true since type of {} is "object"
✓ should return true since type of [] is "object"
✓ should return false since type of 5 is not "object"
✓ should return false since type of "object" is not "object"
...
概括
这是一篇入门文章,我们学习了测试的基础知识、JavaScript 中最常见的测试类型,以及如何使用测试框架 Jest 测试我们的单元。现在我们知道,要测试代码,需要同时使用test()
和expect()
函数。我们还知道,可以使用describe()
函数将逻辑相似的测试归为同一范围,并使用 函数可以在不同的测试用例下重用相同的测试each
。
感谢您的阅读,下次再见!
文章来源:https://dev.to/rafavls/testing-javascript-with-jest-unit-testing-jmb📖 注意
这篇文章最初发表在我的网站上,如果您想阅读更多类似的内容,请去查看。