测试的意义是什么?
在编程术语中,测试意味着检查我们的代码是否符合某些期望。例如:一个名为 “ transformer” 的函数应在给定某些输入的情况下返回期望的输出。
测试类型很多,但简单来说测试分为三大类:
- 单元测试
- 集成测试
- UI 测试
在本 Jest 教程中,我们将仅介绍单元测试,但是在本文结尾,您将找到其他类型的测试的资源。
Jest 教程:什么是 Jest
Jest 是 JavaScript 测试运行程序,即用于创建,运行和构建测试的 JavaScript 库。Jest 是作为 NPM 软件包分发的,您可以将其安装在任何 JavaScript 项目中。 Jest 是目前最受欢迎的测试运行程序之一(我觉得没有之一),也是 Create React App 的默认选择。
首先,我怎么知道要测试什么?
在测试方面,即使是简单的代码块也可能使新手懵逼。最常见的问题是 “我怎么知道要测试什么?”。如果你正在编写 Web 应用程序,那么一个好的切入点就是测试应用程序的每个页面以及每个用户的交互。但是,Web 应用程序也由功能和模块之类的代码单元组成,也需要进行测试。大多数情况下有两种情况:
- 你继承了未经测试的旧代码
- 你从 0 开始新实现的功能
该怎么做呢?对于这两种情况,你都可以通过将测试视为代码的一部分来进行检查,这些代码可以检查给定的函数是否产生预期的结果。典型的测试流程如下所示:
- 导入到测试的功能
- 给定一个输入
- 定义期望的输出
- 检查函数是否产生预期的输出
真的,就是这样。如果你从以下角度考虑,测试将不再可怕:输入-预期输出-声明结果。稍后,我们还将看到一个方便的工具,用于几乎准确地检查要测试的内容。现在先用 Jest 手动测试!
Jest 教程: 初始化项目
与每个 JavaScript 项目一样,您将需要一个 NPM 环境(确保在系统上安装了 Node)。创建一个新文件夹并使用以下命令初始化项目:
1 2 |
mkdir getting-started-with-jest && cd $_ npm init -y |
下一步安装 Jest:
1 |
npm i jest --save-dev |
我们还需要配置一个 script,以便从命令行运行测试。打开 package.json 并配置名为 “ test” 的脚本以运行 Jest:
1 2 3 |
"scripts": { "test": "jest" }, |
现在,你可以嗨皮的开始(入坑)了。
Jest 教程:规范和测试驱动的开发
作为开发人员,我们都喜欢创造自由。但是,当涉及到严重的问题时,大多数时候没有那么多特权。通常,我们必须遵循规范,即对构建内容的书面或口头描述。
在本教程中,我们的项目经理提供了一个相当简单的规范。非常重要的一点是,业务方需要一个 JavaScript 函数,该函数用来过滤一个对象数组。
对于每个对象,我们必须检查一个名为 “ url” 的属性,如果该属性的值与给定的关键字匹配,则应在结果数组中包括匹配的对象。作为一个精通测试的 JavaScript 开发人员,你希望遵循 TDD(测试驱动开发),这是一种在开始编写代码之前必须编写失败测试的准则。
默认情况下,Jest 希望在项目文件夹中的 “ tests” 文件夹中找到测试文件。
创建新文件夹:
1 2 |
cd getting-started-with-jest mkdir __tests__ |
接下来,在 __tests__
目录中中创建一个名为 filterByTerm.spec.js 的新文件。你可能想知道为什么扩展名包含 “ .spec”。这是从 Ruby 借来的约定,用于将文件标记为给定功能的规范。
现在,让我们进行测试!
Jest 教程:测试结构和第一个失败的测试
是时候创建你的第一个 Jest 测试了。打开 filterByTerm.spec.js 并创建一个测试块:
1 2 3 |
describe("Filter function", () => { // test stuff }); |
我们的第一个朋友 describe, 一种用于包含一个或多个相关测试的 Jest 方法。
每次在开始为功能编写新的测试套件时,都将其包装在 describe 块中。
如你所见,它带有两个参数:用于描述测试套件的字符串和用于包装实际测试的回调函数。
接下来,我们将遇到另一个称为 test 的函数,它是实际的测试块:
1 2 3 4 5 |
describe("Filter function", () => { test("it should filter by a search term (link)", () => { // actual test }); }); |
至此,我们准备编写测试了。请记住,测试是输入,功能和预期输出的问题。首先让我们定义一个简单的输入,即对象数组:
1 2 3 4 5 6 7 8 |
test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; }); }); |
接下来,我们将定义预期的结果。根据规范,被测函数应忽略其 url 属性与给定搜索词不匹配的对象。例如,我们可以期望一个带有单个对象的数组,给定 “link” 作为搜索词:
1 2 3 4 5 6 7 8 9 10 11 |
describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; }); }); |
现在我们准备编写实际的测试。我们将使用 expect 和 Jest 匹配器来检查虚拟函数(目前)在调用时是否返回了预期结果。这是测试:
1 |
expect(filterByTerm(input, "link")).toEqual(output); |
为了进一步分解内容,这是在代码中调用该函数的方式:
1 |
filterByTerm(inputArr, "link"); |
在 Jest 测试中,你应该将函数调用包装在 expect 中,并与匹配器(用于检查输出的 Jest 函数)一起进行实际测试。这是完整的测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; expect(filterByTerm(input, "link")).toEqual(output); }); }); |
(要了解有关 Jest 匹配器的更多信息,请查阅文档)。
到这里,你可以尝试:
1 |
npm test |
你会看到,测试失败了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
FAIL __tests__/filterByTerm.spec.js Filter function ✕ it should filter by a search term (2ms) ● Filter function › it should filter by a search term (link) ReferenceError: filterByTerm is not defined 9 | const output = [{ id: 3, url: "https://www.link3.dev" }]; 10 | > 11 | expect(filterByTerm(input, "link")).toEqual(output); | ^ 12 | }); 13 | }); 14 | |
"ReferenceError: filterByTerm is not defined". 实际上,这是好事,让我们在下一节中修复它。
Jest 教程: 修复测试(并再次让它失败)
真正缺少的是 filterByTerm 的实现。为了方便起见,我们将在测试所在的同一文件中创建函数。在真实的项目中,你应该在另一个文件中定义该函数,然后从测试文件中将其导入。
为了使测试通过,我们将使用一个名为 filter 的本地 JavaScript 函数,该函数能够从数组中滤除元素。这是 filterByTerm 的最小实现:
1 2 3 4 5 |
function filterByTerm(inputArr, searchTerm) { return inputArr.filter(function(arrayElement) { return arrayElement.url.match(searchTerm); }); } |
它是这样工作的:对于输入数组的每个元素,我们检查 “ url” 属性,并使用 match 方法将其与正则表达式进行匹配。
这是完整的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function filterByTerm(inputArr, searchTerm) { return inputArr.filter(function(arrayElement) { return arrayElement.url.match(searchTerm); }); } describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; expect(filterByTerm(input, "link")).toEqual(output); }); }); |
现在,再次运行测试:
1 |
npm test |
然后可以看到测试通过了!
1 2 3 4 5 6 7 8 |
PASS __tests__/filterByTerm.spec.js Filter function ✓ it should filter by a search term (link) (4ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.836s, estimated 1s |
流弊!但是我们完成测试了吗?还没。使我们的功能失败需要什么?让我们用大写搜索词来强调该函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function filterByTerm(inputArr, searchTerm) { return inputArr.filter(function(arrayElement) { return arrayElement.url.match(searchTerm); }); } describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; expect(filterByTerm(input, "link")).toEqual(output); expect(filterByTerm(input, "LINK")).toEqual(output); // New test }); }); |
运行测试,你会发现测试失败了,让我们再次修复它。
Jest 教程: 修复大小写问题
filterByTerm 还应考虑大写搜索词。换句话说,即使搜索词是大写字符串,它也应返回匹配的对象:
1 2 |
filterByTerm(inputArr, "link"); filterByTerm(inputArr, "LINK"); |
为了测试这种情况,我们引入了一个新的测试:
1 |
expect(filterByTerm(input, "LINK")).toEqual(output); // New test |
为了使其通过,我们可以调整提供的正则表达式以匹配:
1 2 3 |
// return arrayElement.url.match(searchTerm); // |
除了可以直接传递 searchTerm 之外,我们可以构造一个不区分大小写的正则表达式,即无论字符串大小写如何都匹配的表达式。解决方法是:
1 2 3 4 5 6 |
function filterByTerm(inputArr, searchTerm) { const regex = new RegExp(searchTerm, "i"); return inputArr.filter(function(arrayElement) { return arrayElement.url.match(regex); }); } |
这是完整的测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; expect(filterByTerm(input, "link")).toEqual(output); expect(filterByTerm(input, "LINK")).toEqual(output); }); }); function filterByTerm(inputArr, searchTerm) { const regex = new RegExp(searchTerm, "i"); return inputArr.filter(function(arrayElement) { return arrayElement.url.match(regex); }); } |
再次运行并看到它通过。做得好!作为练习,你可以编写两个新测试并检查以下条件:
- 测试搜索字词 “ uRl”
- 测试一个空的搜索词。函数应如何处理?
你将如何组织这些新测试?
在下一节中,我们将看到测试中的另一个重要主题:代码覆盖率。
Jest 教程: 代码覆盖率
什么是代码覆盖率?在谈论它之前,让我们快速调整我们的代码。在项目根目录 src 中创建一个新文件夹,并创建一个名为 filterByTerm.js 的文件,我们将在其中放置和导出函数:
1 2 |
mkdir src && cd _$ touch filterByTerm.js |
filterByTerm.js 内容:
1 2 3 4 5 6 7 8 |
if (!searchTerm) throw Error("searchTerm cannot be empty"); const regex = new RegExp(searchTerm, "i"); return inputArr.filter(function(arrayElement) { return arrayElement.url.match(regex); }); } module.exports = filterByTerm; |
现在,让我们假装我是新来的。我对测试一无所知,我没有要求更多的上下文,而是直接进入该函数以添加新的 if 语句:
1 2 3 4 5 6 7 8 9 10 |
function filterByTerm(inputArr, searchTerm) { if (!searchTerm) throw Error("searchTerm cannot be empty"); if (!inputArr.length) throw Error("inputArr cannot be empty"); // new line const regex = new RegExp(searchTerm, "i"); return inputArr.filter(function(arrayElement) { return arrayElement.url.match(regex); }); } module.exports = filterByTerm; |
filterByTerm 中有一行新代码,似乎将不进行测试。除非我告诉你 “有一个要测试的新语句”,否则你不会确切知道要在我们的函数中进行什么样的测试。几乎无法想象出,我们的代码的所有可执行路径,因此需要一种工具来帮助发现这些盲点。
该工具称为代码覆盖率,是我们工具箱中的强大工具。 Jest 具有内置的代码覆盖范围,你可以通过两种方式激活它:
- 通过命令行传递标志 “ --coverage”
- 在 package.json 中配置 Jest
在进行覆盖测试之前,请确保将 filterByTerm 导入 tests / filterByTerm.spec.js:
1 2 |
const filterByTerm = require("../src/filterByTerm"); // ... |
保存文件并进行覆盖测试:
1 |
npm test -- --coverage |
你会看到下面的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
PASS __tests__/filterByTerm.spec.js Filter function ✓ it should filter by a search term (link) (3ms) ✓ it should filter by a search term (uRl) (1ms) ✓ it should throw when searchTerm is empty string (2ms) -----------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | -----------------|----------|----------|----------|----------|-------------------| All files | 87.5 | 75 | 100 | 100 | | filterByTerm.js | 87.5 | 75 | 100 | 100 | 3 | -----------------|----------|----------|----------|----------|-------------------| Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total |
对于我们的函数,一个非常好的总结。可以看到,第三行没有被测试覆盖到。下面来通过添加新的测试代码,让覆盖率达到 100%。
如果要保持代码覆盖率始终处于开启状态,请在 package.json 中配置 Jest,如下所示:
1 2 3 4 5 6 |
"scripts": { "test": "jest" }, "jest": { "collectCoverage": true }, |
还可以将标志传递给测试脚本:
1 2 3 |
"scripts": { "test": "jest --coverage" }, |
如果您是一个有视觉素养的人,那么也可以使用一种 HTML 报告来覆盖代码,这就像配置 Jest 一样简单:
1 2 3 4 5 6 7 |
"scripts": { "test": "jest" }, "jest": { "collectCoverage": true, "coverageReporters": ["html"] }, |
现在,每次运行 npm test 时,您都可以在项目文件夹中访问一个名为 coverage 的新文件夹:jest / jest / coverage /。在该文件夹中,您会发现一堆文件,其中/coverage/index.html 是代码覆盖率的完整 HTML 摘要:
如果单击函数名称,你还将看到确切的未经测试的代码行:
非常整洁不是吗?通过代码覆盖,你可以发现有疑问时要测试的内容。
Jest 教程:如何测试 React?
React 是一个超级流行的 JavaScript 库,用于创建动态用户界面。 Jest 可以顺利地测试 React 应用(Jest 和 React 都来自 Facebook 的工程师)。 Jest 还是 Create React App 中的默认测试运行程序。
如果您想学习如何测试 React 组件,请查看 《测试 React 组件:最权威指南》。该指南涵盖了单元测试组件,类组件,带有钩子的功能组件以及新的 Act API。
结论(从这里开始)
测试是一个巨大而有趣的话题。有许多类型的测试和许多测试库。在本 Jest 教程中,你学习了如何配置 Jest 进行覆盖率报告,如何组织和编写简单的单元测试以及如何测试 JavaScript 代码。
要了解有关 UI 测试的更多信息,我强烈建议您看一下 Cypress 的 JavaScript 端到端测试。
如果你准备好想入坑自动化测试和集成测试,那么在 js 中进行自动化测试和集成测试会非常适合你。
您可以在 Github 上找到本教程的代码:getting-started-with-jest。
感谢您的阅读和关注!