原文: Test-Driven Development Tutorial – How to Test Your JavaScript and ReactJS Applications,作者:Oluwatobi Sofela

译者: PapayaHUANG

想要成为高产的软件开发工程师,了解测试驱动的开发必不可少。测试是创建可靠程序的基石。

这篇教程会帮助你在JavaScript和React应用中实现测试驱动的开发。

目录

  1. 什么是测试驱动开发
  2. 测试驱动开发工作流的JavaScript示例
  3. 如何使用Jest来测试执行
  4. 在Jest中使用es6模块须知
  5. 测试驱动的开发有什么好处
  6. 测试驱动开发中的单元测试是什么
  7. 测试驱动开发中的集成测试是什么
  8. 测试驱动开发中的端到端测试是什么
  9. 测试驱动开发中的测试替身是什么
  10. 阶段性总结测试驱动开发
  11. 如何测试React组件
  12. 测试运行工具vsReact组件测试工具:区别是什么
  13. 项目:React测试如何运行
  14. 总结

话不多说,让我们开始从了解什么是测试驱动开发开始吧!

什么是测试驱动开发

测试驱动开发(TDD) 是一种编程实践,你先写出你预期的程序会产生的结果,再编写程序。

也就是说,TDD需要你预先构思好程序的输出,来通过你展望想实现的功能的测试。

所以,一种高效实践TDD的方法是你首先编写测试你预期结果的程序。

然后,你创建可以通过测试的程序。

举个例子,假设你想要创建一个加法计算器,TDD方法如图:

测试驱动开发工作流示意图
测试驱动开发工作流示意图
  1. 编写一个测试,指定你希望计算器产生的结果
  2. 开发计算器,然后通过预先写好的测试
  3. 执行测试,检查计算器是否通过
  4. 重构测试代码(如有必要)
  5. 重构程序(如有必要)
  6. 重复循环,直至计算器符合你的预期

让我们来看一个用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();

在StackBlitz查看示例

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();

在StackBlitz查看示例

注意在以上例子中,我们没有使用任何第三方库。

其实你可以使用强大的第三方库来执行测试,如:JasmineMochaTapeJest,这些库可以使你的测试运行得更加快速、简洁并充满乐趣。

让我们一起看看如何使用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);
});

在上述代码块中:

  1. 我们将additionCalculator.js项目文件导入到 additionCalculator.test.js测试文件。
  2. 我们编写了一个测试案例,希望当用户提供的参数46的时候,additionCalculator()程序的输出是 10

注意:

  • test()是Jest的全局方法,接受三个参数:
    1. 测试名 ("addition of 4 and 6 to equal 10")
    2. 一个包含你期望测试结果的函数
    3. 一个可选的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.jsontest区域添加 --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()接受两个参数:

  1. 你希望的测试案例组的名字,如: "additionCalculator's test cases".
  2. 包含测试案例的函数

第十三步:重构程序

在重构了测试代码之后,让我们重构一下additionalCalculator程序。

// additionCalculator.js

function additionCalculator(...numbers) {
  return numbers.reduce((sum, item) => sum + item, 0);
}

module.exports = additionCalculator;

在代码块中我们做了这些事情:

  1. ...numbers代码使用了JavaScript中的展开操作符 (...) ,将函数的参数转化为一个数组。
  2. 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仅在生产阶段,而不在开发阶段提供服务。所以,你有两种选择:

  1. 一直等到应用投入使用(这可能要等上数月);
  2. 克隆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组件的工具是:

  1. 测试运行工具
  2. React组件测试工具

测试运行工具和React组件测试工具的主要区别是什么?

测试运行工具 vs React组件测试工具:区别是什么?

以下是测试运行工具和React组件测试工具的主要区别:

什么是测试运行

测试运行是一种测试工具,执行测试脚本,并将结果打印在命令行(CLI)。

假设你想要执行你的项目中App.test.js的测试脚本中的测试案例,你就可以使用测试运行。

测试运行执行App.test.js,并将结果打印在命令行。

典型的测试运行工具有:JasmineMochaTapeJest

什么是React组件测试工具

React组件测试工具提供强大的API来定义组件测试案例。

假设你需要测试你的项目的<App />组件,你可以使用React组件测试工具来定义组件的测试案例。

也就是说,这个测试工具提供API来编写组件的测试案例。

典型的组件测试工具有: EnzymeReact Testing Library

现在你已经知道了测试运行工具和React组件测试工具是什么,让我们来利用一个简单的项目例子进一步了解React测试是如何运行的。

项目:React测试如何运行

在接下来的例子中,我们将使用JestReact 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提供userEventAPI来模拟在web上用户和应用的交互。

注意: @testing-library/user-eventfireEvent 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);
});

上面的测试代码片段主要做了这些事:

  1. 引入了测试案例需要的包
  2. 编写了测试案例,希望 <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);
  });
});

上面的测试代码片段的重要内容是:

  1. 引入了测试案例需要的包。
  2. 编写了测试案例,希望 <App />组件可以渲染一个head元素包含 "codesweetly test"文本。
  3. 编写了另一个测试案例,模仿用户和应用按钮元素的互动。 也就是说,我们希望一旦用户点击按钮, <App />的head就会更新"a codesweetly project"文本。

注意:

  • describe()是Jest的一个全局方法。这是一个可选的方法,用户将相关的测试案例分组到一起。 describe()接受两个参数:
    • 你希望测试案例组被命名的名称,如: "App component".
    • 包含测试案例的函数。
  • userEvent 包含许多模拟用户与应用交互方法的一个React Testing Library包。例如在代码块中,我们使用 userEventclick()方法来模拟按钮元素的点击事件。
  • 每次测试案例我们都会渲染<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;

在上述代码片段中主要发生了:

  1. Appheading初始状态是"CodeSweetly Test"字符串。
  2. 编写了一个handleClick函数来处理heading状态。
  3. 在DOM渲染一个 <h1>和一个<button>元素。

注意以下几点:

  • <h1>的内容是 heading状态的当前值。
  • 每当用户点击按钮元素, onClick()事件监听器就会调用handleClick()函数。 handleClick就会更新Appheading的状态到"A CodeSweetly Project"。因此 <h1>的内容会改成"A CodeSweetly Project"

第十四步:重新执行测试

一旦重构了组件之后,重新执行测试(或者检查正在运行的测试)来确保应用按照预期执行。

之后,在浏览器查看最近的更新。

就这么多!

恭喜你!你成功地使用Jest和React测试库来测试React组件! 🎉

总结

本文探讨了在JavaScript和ReactJS应用中如何使用测试驱动的开发。

我们还学习了如何使用Jest和React测试库使得测试更加简单快速。

感谢阅读!

这里还有一些有用的ReactJS的资源:

我编写了一本React相关的书籍!

  • 初学者友好 ✔
  • 包含代码片段 ✔
  • 包含可以扩展的项目 ✔
  • 和非常多好理解的例子 ✔

React Explained Clearly是你了解ReactJS的敲门砖。

React Explained Clearly Book Now Available at Amazon