原文: Test-Driven Development Tutorial – How to Test Your JavaScript and ReactJS Applications
想要成为高产的软件开发工程师,了解测试驱动的开发必不可少。测试是创建可靠程序的基石。
这篇教程会帮助你在JavaScript和React应用中实现测试驱动的开发。
目录
- 什么是测试驱动开发
- 测试驱动开发工作流的JavaScript示例
- 如何使用Jest来测试执行
- 在Jest中使用es6模块须知
- 测试驱动的开发有什么好处
- 测试驱动开发中的单元测试是什么
- 测试驱动开发中的集成测试是什么
- 测试驱动开发中的端到端测试是什么
- 测试驱动开发中的测试替身是什么
- 阶段性总结测试驱动开发
- 如何测试React组件
- 测试运行工具vsReact组件测试工具:区别是什么
- 项目:React测试如何运行
- 总结
话不多说,让我们开始从了解什么是测试驱动开发开始吧!
什么是测试驱动开发
测试驱动开发(TDD) 是一种编程实践,你先写出你预期的程序会产生的结果,再编写程序。
也就是说,TDD需要你预先构思好程序的输出,来通过你展望想实现的功能的测试。
所以,一种高效实践TDD的方法是你首先编写测试你预期结果的程序。
然后,你创建可以通过测试的程序。
举个例子,假设你想要创建一个加法计算器,TDD方法如图:

- 编写一个测试,指定你希望计算器产生的结果
- 开发计算器,然后通过预先写好的测试
- 执行测试,检查计算器是否通过
- 重构测试代码(如有必要)
- 重构程序(如有必要)
- 重复循环,直至计算器符合你的预期
让我们来看一个用JavaScript实现的例子
测试驱动开发工作流的JavaScript示例
让我们用一个简单的JavaScript程序,来分步骤实现测试驱动编程的工作流:
1. 编写测试
编写一个测试,指定计算器的输出:
function additionCalculatorTester() {
if (additionCalculator(4, 6) === 10) {
console.log("✔ Test Passed");
} else {
console.error("❌ Test Failed");
}
}
2. 开发程序
编写一个计算器程序以通过编写好的测试
function additionCalculator(a, b) {
return a + b;
}
3. 执行测试
执行测试,查看程序是否通过测试
additionCalculatorTester();
4. 重构测试
在确认程序通过测试之后,可以检查是否需要重构测试代码。
例如,你可以使用三元运算符来重构additionCalculatorTester()
:
function additionCalculatorTester() {
additionCalculator(4, 6) === 10
? console.log("✔ Test Passed")
: console.error("❌ Test Failed");
}
5. 重构程序
让我们使用箭头函数来重构程序:
const additionCalculator = (a, b) => a + b;
6. 执行测试
重新执行测试,确保程序仍然能够通过测试
additionCalculatorTester();
注意在以上例子中,我们没有使用任何第三方库。
其实你可以使用强大的第三方库来执行测试,如:Jasmine、 Mocha、 Tape和Jest,这些库可以使你的测试运行得更加快速、简洁并充满乐趣。
让我们一起看看如何使用Jest。
如何使用Jest来测试执行
在使用Jest工具之前,你需要执行以下步骤:
第一步:使用正确的Node和NPM版本
确保你的系统上装有Node 10.16(或者更高版本)和 NPM 5.6(或者更高版本)。
你可以在Node.js官网下载最新的LTS。
如果你更倾向于使用Yarn,确保你使用Yarn 0.25 (或者更高版本)。
第二步: 创建一个项目目录
为你的项目创建一个目录
mkdir addition-calculator-jest-project
第三步:导航到你的项目文件夹
使用命令行导航到你的项目文件夹
cd path/to/addition-calculator-jest-project
第四步:创建一个package.json
文件
在项目中初始化 package.json
文件
npm init -y
如果你的包管理器是Yarn,执行:
yarn init -y
第五步:安装Jest
把Jest作为开发依赖包安装
npm install jest --save-dev
如果你使用的是Yarn,执行:
yarn add jest --dev
第六步:设置Jest为项目测试运行工具
打开package.json
文件,并把Jest添加到test
区域。
{
"scripts": {
"test": "jest"
}
}
第七步:创建项目文件
创建一个文件,在这个文件里编写开发代码
touch additionCalculator.js
第八步:创建测试文件
创建一个编写测试案例的文件
touch additionCalculator.test.js
注意: 测试文件的结尾必须是 .test.js
,这样Jest才能够分辨出来这个文件是测试文件。
第九步:编写测试案例
打开测试文件,编写你希望程序产出的指定结果。
例子:
// additionCalculator.test.js
const additionCalculator = require("./additionCalculator");
test("addition of 4 and 6 to equal 10", () => {
expect(additionCalculator(4, 6)).toBe(10);
});
在上述代码块中:
- 我们将
additionCalculator.js
项目文件导入到additionCalculator.test.js
测试文件。 - 我们编写了一个测试案例,希望当用户提供的参数是
4
和6
的时候,additionCalculator()
程序的输出是10
。
注意:
test()
是Jest的全局方法,接受三个参数:- 测试名 (
"addition of 4 and 6 to equal 10"
) - 一个包含你期望测试结果的函数
- 一个可选的timeout参数
- 测试名 (
expect()
是一个测试代码输出的Jest方法。toBe()
是一个Jest匹配器函数,可以对比expect()
参数和原始值。
假设你现在执行这个测试,测试将不会通过,因为你还没有编写程序,让我们现在开始吧!
第十步:开发程序
打开项目文件,开发可以通过测试的程序。
这里是例子:
// additionCalculator.js
function additionCalculator(a, b) {
return a + b;
}
module.exports = additionCalculator;
上面的代码块创建了一个additionCalculator()
程序,并且使用module.exports
方法将程序导出。
第十一步:执行测试
执行测试,查看程序是否通过:
npm run test
也可以使用Yarn:
yarn test
假设你的项目有多个测试文件,你想执行其中一个,你可以通过以下代码实现:
npm run test additionCalculator.test.js
如果使用Yarn的话是这样:
yarn test additionCalculator.test.js
一旦你启动了测试,Jest会在你的编辑器控制台打印出通过或者不通过的消息,消息如下:
$ jest
PASS ./additionCalculator.test.js
√ addition of 4 and 6 to equal 10 (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.002 s
Ran all test suites.
Done in 7.80s.
如果你希望Jest自动执行测试,可以在package.json
的test
区域添加 --watchAll
选项。
例子:
{
"scripts": {
"test": "jest --watchAll"
}
}
添加--watchAll
后,重新执行npm run test
(或 yarn test
)命令,Jest会在每次保存后重新执行测试。
注意: 你可以使用键盘上的Q键退出监视(watch)模式。
第十二步:重构测试代码
我们已经确认了程序可以如预期执行,是时候来检查是否需要重构测试代码了。
例如,假设你想要additionalCalculator
允许用户输入任意数量的数字。你可以这样重构你的代码:
// additionCalculator.test.js
const additionCalculator = require("./additionCalculator");
describe("additionCalculator's test cases", () => {
test("addition of 4 and 6 to equal 10", () => {
expect(additionCalculator(4, 6)).toBe(10);
});
test("addition of 100, 50, 20, 45 and 30 to equal 245", () => {
expect(additionCalculator(100, 50, 20, 45, 30)).toBe(245);
});
test("addition of 7 to equal 7", () => {
expect(additionCalculator(7)).toBe(7);
});
test("addition of no argument provided to equal 0", () => {
expect(additionCalculator()).toBe(0);
});
});
注意上面的代码块中的describe()方法,是可选的。这个方法可以帮助将同类型的测试分门别类在一起。
describe()
接受两个参数:
- 你希望的测试案例组的名字,如:
"additionCalculator's test cases"
. - 包含测试案例的函数
第十三步:重构程序
在重构了测试代码之后,让我们重构一下additionalCalculator
程序。
// additionCalculator.js
function additionCalculator(...numbers) {
return numbers.reduce((sum, item) => sum + item, 0);
}
module.exports = additionCalculator;
在代码块中我们做了这些事情:
...numbers
代码使用了JavaScript中的展开操作符 (...
) ,将函数的参数转化为一个数组。numbers.reduce((sum, item) => sum + item, 0)
代码使用JavaScript的reduce()方法,求和了numbers
数组中的所有元素。
第十四步:重新执行测试
重构程序之后,可以重新执行测试,查看是否通过。
结束
恭喜你!你成功地使用Jest,并借助测试驱动开发的方法创建了一个计算器程序。 🎉
在Jest中使用es6模块须知
目前,Jest不能识别ES6模块。
假设,你习惯使用ES6的import/export声明,你必须采取以下步骤:
1. 安装Babel作为依赖包
npm install @babel/preset-env --save-dev
或者使用Yarn:
yarn add @babel/preset-env --dev
2. 在项目的root创建 .babelrc
文件:
touch .babelrc
3. 打开 .babelrc
文件,并且复制以下代码:
{ "presets": ["@babel/preset-env"] }
这样设置之后,上一章节第九步的 require()
声明,可以从
const additionCalculator = require("./additionCalculator");
...变成:
import additionCalculator from "./additionCalculator";
同样的,你也可以替换掉第十步的export
声明,从
module.exports = additionCalculator;
到:
export default additionCalculator;
注意: Jest在使用Babel文档中,指定了类似说明。
4. 重新执行测试
你可以重新执行测试,确保程序仍然通过测试。
现在你已经知道测试驱动的开发是什么,让我们来看看这一方法有什么好处。
测试驱动的开发有什么好处?
在你的开发工作流中引入测试驱动开发(TDD)有以下两大好处:
1. 理解程序的目的
测试驱动的开发可以帮助你理解程序的目的。
也就是说,因为你在编写实际的程序前已经编写了测试,所以TDD可以促使你去思考你想要程序做什么事。
在你通过一到两个测试记录下来你的程序的目的之后,你可以自信地去创建程序。
因此,TDD可以有效地帮助你记录下来你希望程序产生的结果。
2. 信心助推器
TDD是了解你的程序是否如预期工作的的一个基准。它给予你信心,相信自己的程序正确执行。
所以无论之后你的代码库会有什么变化,TDD都可以有效地确保你的程序能够执行。
让我们现在来讨论一下TDD的术语: "单元测试(unit test)"、 "集成测试(integration test)"、 "端对端(E2E)"、和 "测试替身(test doubles)"。
测试驱动开发中的单元测试是什么
单元测试是用于评估程序独立功能的测试。换句话说,单元测试检查一个完全独立的程序单元是不是按照预期工作。
我们为additionalCalculator
程序编写的第十步里的测试就是一个完美的例子。
第十步里的additionalCalculator()
测试是一个独立的函数,不依赖任何外部代码。
注意单元测试首要目的并不是检查是否有bug,而是检查程序的一个独立片段(被称作单元)是否在不同的情况下按照预期工作。
测试驱动开发中的集成测试是什么?
集成测试评估依赖程序的功能。也就是说,集成测试检查一个程序(依赖其他代码)是不是按照要求工作。
我们为 additionalCalculator
程序编写的第十三步的测试就是一个很好的例子。
第十三步的additionalCalculator()
的测试一个例子是因为这个程序是一个依赖函数,依赖了JavaScript的reduce()方法。
也就是说,我们使用事先编写好的测试案例来测试 additionalCalculator()
和reduce()
。
因此,如果JavaScript把reduce()
规定为一个过时的方法,那么在这个案例中,additionalCalculator
会因为reduce()
方法而无法通过测试。
测试驱动开发中的端到端测试是什么?
端到端(E2E)测试访问用户接口(UI)的功能,也就是说E2E检查UI是否按照意图工作。
可以观看Max的Youtube频道了解更多。
测试驱动开发中的测试替身是什么?
**测试替身(test doubles)**是模仿对象,用于模仿如数据库、库、网络和API等真实的依赖项。
使用测试替身可以绕过程序真实的依赖对象,你可以独立于任何依赖项来测试你的代码。
假设你需要测试应用的一个错误是由外部API还是你自己的代码引起的。
但这个API仅在生产阶段,而不在开发阶段提供服务。所以,你有两种选择:
- 一直等到应用投入使用(这可能要等上数月);
- 克隆API,这样不论这个依赖项是否可用,你都可以继续测试。
使用测试替身来克隆项目依赖项,能够帮助你在不打断进度的情况下进行应用测试。
测试替身的典型示例是虚拟对象(dummy)、模拟(mock)、桩(stub)和仿冒(fake)。
测试驱动开发中的虚拟对象(dummy)是什么
虚拟对象(dummy) 是用于模仿特定依赖项的值的测试替身。
假设你的应用依赖一个第三方的方法来提供一些参数。虚拟对象可以传入虚假的值给需要的方法提供参数。
测试驱动开发中的模拟(mock)是什么
模拟(mock) 是用于模仿外部依赖项的测试替身,使用模拟可以在开发的过程中不考虑依赖项的返回。
假设你的应用依赖第三方API(如:Facebook),而这个API不可以在开发模式中被访问。使用模拟可以绕过这个API,这样你可以在不考虑Facebook的API是否可以访问的情况下进行测试。
测试驱动开发中的桩(stub)是什么
桩(stub) 使用手动输入的值来模仿外部依赖项的返回值。你可以使用不同的返回值来测试应用的性能。
假设你的应用依赖于第三方API(如:Facebook),而这个API不可以在开发模式中被访问。桩模仿Facebook的返回值让你可以绕开这个API做测试。
因此,桩可以帮助你获取不同响应场景的应用行为。
测试驱动开发中的仿冒(fake)是什么
仿冒(fake) 是用于创建有动态值的外部依赖项的测试替身。
例如你可以使用仿冒来创建一个本地数据库,来测试你的程序如何和实际数据库一起协同工作的。
阶段性总结测试驱动开发
我们学习了测试驱动开发如何在创建程序前记录程序的行为。
我们也实践了一个简单的JavaScript测试,并且使用Jest来作为测试的工具。
现在让我们一起来看看如何测试React组件。
如何测试React组件
两个主要的测试React组件的工具是:
- 测试运行工具
- React组件测试工具
测试运行工具和React组件测试工具的主要区别是什么?
测试运行工具 vs React组件测试工具:区别是什么?
以下是测试运行工具和React组件测试工具的主要区别:
什么是测试运行
测试运行是一种测试工具,执行测试脚本,并将结果打印在命令行(CLI)。
假设你想要执行你的项目中App.test.js
的测试脚本中的测试案例,你就可以使用测试运行。
测试运行执行App.test.js
,并将结果打印在命令行。
典型的测试运行工具有:Jasmine、 Mocha、 Tape和Jest。
什么是React组件测试工具
React组件测试工具提供强大的API来定义组件测试案例。
假设你需要测试你的项目的<App />
组件,你可以使用React组件测试工具来定义组件的测试案例。
也就是说,这个测试工具提供API来编写组件的测试案例。
典型的组件测试工具有: Enzyme 和 React Testing Library。
现在你已经知道了测试运行工具和React组件测试工具是什么,让我们来利用一个简单的项目例子进一步了解React测试是如何运行的。
项目:React测试如何运行
在接下来的例子中,我们将使用Jest和React Testing Library (文档由Kent C. Dodds编写)来学习React测试是如何运行的。
注意: React官方文档推荐结合Jest和React Testing Library一起来测试React组件。
第一步:获取正确的Node和NPM版本
确保你的系统安装的是Node 10.16 (或者更高版本)以及NPM 5.6 (或者更高版本)。
如果你倾向于使用Yarn,确保你安装的是Yarn 0.25 (或者更高版本).
第二步:创建一个新的React应用
使用NPM的create-react-app包来创建一个名为react-testing-project
的项目:
npx create-react-app react-testing-project
同样,你可以使用Yarn来创建:
yarn create react-app react-testing-project
第三步:导航进入项目目录
创建完毕后,导航进入到项目目录
cd react-testing-project
第四步:设置测试环境
安装下列测试包
- jest
- @testing-library/react
- @testing-library/jest-dom
- @testing-library/user-event
注意: 如果你是通过create-react-app
(第二步)来初始化你的项目,你就不需要安装这些测试包。这些测试包已经被预安装到了package.json
文件中。
现在让我们讲解一下这些测试包的作用:
什么是Jest
jest是个测试运行工具,我们可以使用这个工具来运行测试脚本,并将结果打印在命令行。
什么是@testing-library/react
@testing-library/react是一个React测试库,提供为React组件编写测试案例的API。
什么是@testing-library/jest-dom
@testing-library/jest-dom提供定制的Jest匹配器来测试DOM的状态。
注意: Jest已经包含很多匹配器,所以使用jest-dom
是可选的。 jest-dom
只是扩展了Jest匹配器,使得测试更加声明式、易阅读以及更容易维护。
什么是@testing-library/user-event
@testing-library/user-event提供userEvent
API来模拟在web上用户和应用的交互。
注意: @testing-library/user-event
比fireEvent API更好用。
第五步: 清空src
文件夹
删除所有在src
文件夹里的文件。
第六步: 创建代码文件
在src
文件夹中创建以下文件:
index.js
App.js
App.test.js
第七步:渲染App
组件
打开index.js
文件,并在DOM渲染App
组件:
// index.js
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
// 在根DOM渲染APP组件
createRoot(document.getElementById("root")).render(<App />);
第八步:创建测试案例
假设你希望App.js
文件在网页渲染一个<h1>CodeSweetly Test</h1>
元素。打开 test script 并编写你希望 <App />
组件生产的结果。
例子:
// App.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import App from "./App";
test("codesweetly test heading", () => {
render(<App />);
expect(screen.getByRole("heading")).toHaveTextContent(/codesweetly test/i);
});
上面的测试代码片段主要做了这些事:
- 引入了测试案例需要的包
- 编写了测试案例,希望
<App />
组件可以渲染一个head元素包含"codesweetly test"
文本。
test()
是Jest的一个全局方法。我们使用它运行测试案例。这个方法接受三个参数:- 测试名(
"codesweetly test heading"
) - 包含期望测试结果的函数
- 可选的timeout参数
- 测试名(
render()
是React Testing library的一个API,我们使用它来渲染我们希望测试的组件。expect()
是一个测试代码结果的Jest方法。screen
是一个包含多种搜寻页面元素方法的React Testing Library对象。getByRole()
是搜寻页面元素的一个React Testing Library的请求方法。toHaveTextContent()
是jest-dom
的一个定制匹配器,可以使用它来确认特定节点存在文本内容。/codesweetly test/i
是一个正则表达式 语法,用于表达搜索不区分大小写的codesweetly test
。
记住有三种方式来编写上面的声明:
// 1. 使用jest-dom'的toHaveTextContent()方法:
expect(screen.getByRole("heading")).toHaveTextContent(/codesweetly test/i);
// 2. 使用头部的textContent属性和Jest的toMatch()方法:
expect(screen.getByRole("heading").textContent).toMatch(/codesweetly test/i);
// 3. 使用React Testing Library的名称选项和jest-dom的toBeInTheDocument()方法:
expect(screen.getByRole("heading", { name: /codesweetly test/i })).toBeInTheDocument();
提示:
可以添加level
选项到getByRole()
方法,来标注head的层级。
例子:
test("codesweetly test heading", () => {
render(<App />);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(/codesweetly test/i);
});
level: 1
代表了<h1>
元素。
假设你现在运行测试,会测试失败,因为还没有编写组件,所以我们现在开始编写:
第九步:开发你的React组件
打开App.js
文件来开发一个可以通过测试的组件
例子:
// App.js
import React from "react";
const App = () => <h1>CodeSweetly Test</h1>;
export default App;
在代码片段中,App
组件渲染了一个<h1>
元素包含了 "CodeSweetly Test"
文本。
第十步:执行测试
执行实现写好的测试,检查测试通过还是失败:
npm test App.test.js
也可以使用Yarn:
yarn test App.test.js
初始化测试后,Jest会在你的编辑器的控制台打印通过或者失败的消息:
$ jest
PASS src/App.test.js
√ codesweetly test heading (59 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.146 s
Ran all test suites related to changed files.
注意: create-react-app
默认在watch mode配置Jest。所以,在执行 npm test
(或者yarn test
)之后,你当前打开的终端会继续执行test
命令的活动。在test
执行的过程中,你将没办法在终端输入任何内容,但是你可以同时期开启一个新的终端窗口来执行test
。
也就是说,使用一个窗口来执行test
,另一个来输入命令。
第十一步:执行应用
在浏览器查看应用:
npm start
如果你的包管理工具 是Yarn,执行:
yarn start
一旦执行上述命令,你的默认浏览器就会自动打开你的应用。
第十二步:重构测试代码
假设你希望当用户点击按钮的时候改变head的文字。你可以模拟一个按钮来测试这个用户交互是否成立。
例子:
// App.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import App from "./App";
describe("App component", () => {
test("codesweetly test heading", () => {
render(<App />);
expect(screen.getByRole("heading")).toHaveTextContent(/codesweetly test/i);
});
test("a codesweetly project heading", () => {
render(<App />);
const button = screen.getByRole("button", { name: "Update Heading" });
userEvent.click(button);
expect(screen.getByRole("heading")).toHaveTextContent(/a codesweetly project/i);
});
});
上面的测试代码片段的重要内容是:
- 引入了测试案例需要的包。
- 编写了测试案例,希望
<App />
组件可以渲染一个head元素包含"codesweetly test"
文本。 - 编写了另一个测试案例,模仿用户和应用按钮元素的互动。 也就是说,我们希望一旦用户点击按钮,
<App />
的head就会更新"a codesweetly project"
文本。
注意:
describe()
是Jest的一个全局方法。这是一个可选的方法,用户将相关的测试案例分组到一起。describe()
接受两个参数:- 你希望测试案例组被命名的名称,如:
"App component"
. - 包含测试案例的函数。
- 你希望测试案例组被命名的名称,如:
userEvent
包含许多模拟用户与应用交互方法的一个React Testing Library包。例如在代码块中,我们使用userEvent
的click()
方法来模拟按钮元素的点击事件。- 每次测试案例我们都会渲染
<App />
,因为每次测试后,React测试库都会卸载掉已经渲染的组件。假设你的组件有多个测试案例, 使用Jest的beforeEach()
方法来渲染你文件中的render(<App />)
(或者describe
代码块中)的测试。
第十三步:重构React组件
我们已经重构了测试代码,现在我们来重构App
组件:
// App.js
import React, { useState } from "react";
const App = () => {
const [heading, setHeading] = useState("CodeSweetly Test");
const handleClick = () => {
setHeading("A CodeSweetly Project");
};
return (
<>
<h1>{heading}</h1>
<button type="button" onClick={handleClick}>
Update Heading
</button>
</>
);
};
export default App;
在上述代码片段中主要发生了:
App
的heading
初始状态是"CodeSweetly Test"
字符串。- 编写了一个
handleClick
函数来处理heading
状态。 - 在DOM渲染一个
<h1>
和一个<button>
元素。
注意以下几点:
<h1>
的内容是heading
状态的当前值。- 每当用户点击按钮元素,
onClick()
事件监听器就会调用handleClick()
函数。handleClick
就会更新App
的heading
的状态到"A CodeSweetly Project"
。因此<h1>
的内容会改成"A CodeSweetly Project"
。
第十四步:重新执行测试
一旦重构了组件之后,重新执行测试(或者检查正在运行的测试)来确保应用按照预期执行。
之后,在浏览器查看最近的更新。
就这么多!
恭喜你!你成功地使用Jest和React测试库来测试React组件! 🎉
总结
本文探讨了在JavaScript和ReactJS应用中如何使用测试驱动的开发。
我们还学习了如何使用Jest和React测试库使得测试更加简单快速。
感谢阅读!
这里还有一些有用的ReactJS的资源:
我编写了一本React相关的书籍!
- 初学者友好 ✔
- 包含代码片段 ✔
- 包含可以扩展的项目 ✔
- 和非常多好理解的例子 ✔
React Explained Clearly是你了解ReactJS的敲门砖。