0%

JavaScript 测试系列实战(一):使用 Jest 和 Enzyme 测试 React 组件

@tuture-dev

查看代码

初识 Jest 单元测试

测试是检查代码的代码,能够大大增强我们对应用的信心。更重要的是,测试会阻止你在修复一件事情的同时破坏另一件事情,让我们能够放开手脚进行功能的添加与大规模重构。您可以测试应用程序的许多方面,从单个函数及其返回值到在浏览器中运行的复杂应用程序。万丈高楼平地起,让我们先来了解一下有哪些测试。

测试的类型

单元测试

单元测试的目标可以是一个函数,一个类,或者一个模块。单元测试应该是相互隔离和独立的。对于给定的输入,单元测试检查结果。通过及早发现问题并避免 bug 回归,它可以帮助我们确保代码的各个部分按预期工作。

集成测试

即使所有单元测试都通过了,我们的应用仍然可能会崩溃。集成测试则是用来测试跨模单元/模块的过程,可以很好地确保我们的代码能够作为一个整体运行。

端到端测试(E2E)

与其他类型的测试不同,E2E 测试总是在浏览器(或类浏览器)环境中运行。它可能是一个实际的浏览器,可以打开并在其中运行测试;也可能是一个无头(Headless)的浏览器环境,这是一个没有用户界面的浏览器。E2E 测试的重点是在我们正在运行的应用程序中模拟实际用户(例如模拟滚动、单击和键入等行为),并检查我们的应用程序是否从实际用户的角度运行良好。

在这一系列教程中,我们将会从零开始,一步步带你熟悉从单元测试到端到端测试的方方面面。我们将会在一个 React 项目中实践所学到的自动化测试技术。首先用 Create React App(CRA)搭建项目脚手架:

create-react-app javascript-test-series

然后我们删除 src 目录下所有预创建的文件(当然你也可以手动删除):

rm src/*

一切准备就绪!让我们开始吧。

编写第一个单元测试

编写一个单元测试实际上要比你想象得简单很多。首先创建 divide.js ,在其中编写一个 divide 函数:

divide.js查看完整代码
function divide(a, b) {
return a / b;
}

module.exports = divide;

然后创建测试文件 divide.test.js ,代码如下:

divide.test.js查看完整代码
const divide = require('./divide');

test('dividing 6 by 3 equals 2', () => {
expect(divide(6, 3)).toBe(2);
});

作为本系列教程的第一个 Jest 测试,我们来详细讲解一下:

  • 我们先导入需要测试的单元/模块
  • test 函数定义了一个测试用例,第一个参数就是用例描述,一般是一句完整的描述,例如上面的 dividing 6 by 3 equals 2 ;第二个参数则是一个待执行的测试函数
  • 在测试函数中,最重要的组成部分就是断言(Assertion),例如上面的 expect(divide(6, 3)).toBe(2)
  • 断言的核心是 expect 函数,它接受一个表达式,然后后面可以调用 Matcher 来测试该表达式是否符合条件,例如这里我们就使用了最常用的 toBe Matcher;Jest 还提供了大量的 Matcher,可以帮助我们写出更简洁可读的断言语句,可参考 Expect API

CRA 已经为我们配置好了 Jest,这里直接运行 npx jest 命令,就可以看到测试结果了:

PASS  ./divide.test.js
✓ dividing 6 by 3 equals 2 (5ms)

提示

CRA 也配置了 test 命令,但是提供了比较复杂的功能配置(例如 Watch 模式等),可能会让初学 Jest 的你不知所措。因此这里建议直接使用 npx jest 执行测试。

编写第一组测试

每个测试文件通常有多个测试用例。Jest 允许我们通过 describe 函数对测试用例进行分组,它创建了一个可以组合多个测试的块。让我们对全局 Math 对象运行一些测试(希望浏览器工程师和 Node 开源项目维护者不要来打我),创建 math.test.js ,代码如下:

math.test.js查看完整代码
describe('in the math global object', () => {
describe('the random function', () => {
it('should return a number', () => {
expect(typeof Math.random()).toEqual('number');
});

it('should return a number between 0 and 1', () => {
const randomNumber = Math.random();
expect(randomNumber).toBeGreaterThanOrEqual(0);
expect(randomNumber).toBeLessThan(1);
});
});

describe('the round function', () => {
it('should return a rounded value of 4.5 being 5', () => {
expect(Math.round(4.5)).toBe(5);
});
});
});

你也许注意到了这里我们用了 it 函数而不是 test 函数,这两者实际上是完全一样的。

这样对测试进行分组可以使我们的代码更加清晰。在关注应用程序的代码质量的同时,我们也应该确保测试代码的质量,这样我们才有足够的动力不断去维护测试代码,从而确保我们的项目能够保持健壮。

除了使代码更具可读性之外,它还有助于在出现错误时提供更好的错误消息。如果这里我们将第一条测试用例改为 expect(typeof Math.random()).toEqual('string') ,那么再运行 npx jest ,就会出现如下错误信息:

FAIL  ./math.test.js
● in the math global object › the random function › should return a number

expect(received).toEqual(expected)

Expected value to equal:
"string"
Received:
"number"

是不是一目了然呢?

小结

在这一小节中,我们首先了解了测试有哪些类型。然后我们在 CRA 脚手架中编写了一个简单的函数,并为之编写了第一个单元测试,熟悉了测试用例、断言、Matcher 这些关键概念,并成功地通过了测试。接着,我们又编写了一个包含多个用例的测试文件,并通过 describe 函数将测试用例组织得井井有条。

初识 Enzyme:编写第一个 React 组件测试

很显然,我们不会仅仅满足于测试像 divide 那样简单的函数,我们希望能够测试一个 React 组件,但是和一个普通的 JavaScript 函数不同,测试一个 React 组件还需要两个关键的问题:1)怎么渲染待测试的组件;2)怎么测试渲染出来的组件。

所幸的是,Airbnb 作为重度使用 React 的先驱,早就提出了专门的解决方案:Enzyme

安装和配置 Enzyme

首先安装 Enzyme 和相应的 React 适配器:

npm install enzyme enzyme-adapter-react-16

我们需要配置一下 Enzyme,才能在 Jest 测试文件中使用它。创建 src/setupTests.js ,代码如下:

src/setupTests.js查看完整代码
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

浅层渲染

Enzyme 提供的一个重要功能便是组件的浅层渲染(Shallow Rendering)。它允许我们在运行测试时,只渲染父组件而不渲染其所有的子组件。浅层渲染十分快速,因此非常适合单元测试。

首先让我们创建一个简单的 React 组件,创建 src/App.js ,代码如下:

src/App.js查看完整代码
import React from 'react';

const App = () => {
return <h1>Hello world!</h1>;
};

export default App;

编写 App 组件对应的测试文件 src/App.test.js ,代码如下:

src/App.test.js查看完整代码
import React from 'react';
import { shallow } from 'enzyme';

import App from './App';

describe('app component', () => {
it('contains a header with the "Hello world!"', () => {
const app = shallow(<App />);
expect(app.containsMatchingElement(<h1>Hello world!</h1>)).toEqual(true);
});
});

可以看到,这里我们用 shallow 函数来浅层渲染 App 组件得到 app ,并且调用其 containsMatchingElement 来判断渲染后的 App 组件是否包含 <h1>Hello world!</h1> 元素。

Enzyme 浅层渲染后的组件还包括其他测试方法,可参考 https://enzymejs.github.io/enzyme/docs/api/shallow.html

通过 npm test 命令,我们就可以看到刚才的测试通过了:

PASS  app/App.test.js
app component
✓ contains a header with the "Hello world!"

测试更复杂的组件

在实际的前端开发中,我们的组件要复杂很多。本着循序渐进的原则,我们稍微前进一步:来编写一个接受 props 的组件,并根据数据来决定渲染结果。

配置 jest-enzyme

你应该还记得,在刚才的测试代码中,我们还是使用了 Jest 自带的 Matcher(toEqual)。但实际上,社区还提供了更好的选择——专门为 Enzyme 定制的 Matcher 库:enzyme-matchers。这些 Matcher 使得编写断言语句更轻松、更具可读性。

我们通过 npm 来安装 jest-enzyme:

npm install jest-enzyme

相应地在 src/setupTests.js 中添加相应的配置:

src/setupTests.js查看完整代码
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
[tuture-add]import 'jest-enzyme';

configure({ adapter: new Adapter() });

编写 TodoList 组件

这次,我们还是编写一个熟悉的 TodoList 组件。创建 src/TodoList.js ,代码如下:

src/TodoList.js查看完整代码
import React from 'react';

const ToDoList = (props) => {
return (
<ul>
{props.tasks.map((taskName, index) => (
<li key={index}>{taskName}</li>
))}
</ul>
);
};

export default ToDoList;

可以看到,这个组件接受一个 tasks 数组,并将其渲染成一个列表。

编写 TodoList 组件测试

先思考一下,如果要测试上面的 TodoList 组件,要考虑哪些情况?不难想到主要是两种情况:

  • 传入的 tasks 数组为空
  • 传入的 tasks 数组不为空

对应这两种情况,我们开始编写测试。创建 src/TodoList.test.js ,代码如下:

src/TodoList.test.js查看完整代码
import React from 'react';
import { shallow } from 'enzyme';

import ToDoList from './ToDoList';

describe('ToDoList component', () => {
describe('when provided with an empty array of tasks', () => {
it('contains an empty <ul> element', () => {
const toDoList = shallow(<ToDoList tasks={[]} />);
expect(toDoList).toContainReact(<ul />);
});

it('does not contain any <li> elements', () => {
const toDoList = shallow(<ToDoList tasks={[]} />);
expect(toDoList.find('li').length).toEqual(0);
});
});

describe('when provided with an array of tasks', () => {
it('contains a matching number of <li> elements', () => {
const tasks = ['Wash the dishes', 'Make the bed'];
const toDoList = shallow(<ToDoList tasks={tasks} />);
expect(toDoList.find('li').length).toEqual(tasks.length);
});
});
});

可以看到在第一个测试用例中,我们使用了 toContainReact 这个 Matcher,它的含义十分明显,一目了然;在后面的测试用例中,我们通过 todoList.find('li') 来获取 li 元素数组,并判断它的长度是否符合要求。

提示

你也许发现我们并没有去验证 TodoList 每一项是否符合,这是因为我们用了 Enzyme 的浅层渲染,这意味着所有的 children 都是处于未渲染状态,当然就无法验证内容是否正确了。我们将在下一篇教程中讲解如何去更“深层”地去测试我们的组件。

运行 npm test ,查看测试结果,全部通过:

PASS  app/App.test.js
PASS app/components/ToDoList/ToDoList.test.js

Test Suites: 2 passed, 2 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 1.41s
Ran all test suites.

小结

在过去的两个小节中,我们了解、安装和配置了 Enzyme,并且接触了 shallow 浅层渲染这个单元测试利器,并且循序渐进测试了两个 React 组件。但是你应该也注意到了,有些时候浅层渲染并不能完全满足我们的需求,Enzyme 还提供了其他渲染方式以供测试。我们在下篇教程中将讲解新的渲染方式,并介绍快照测试以及 mock 数据,不见不散哦!

图雀社区 微信公众号

扫一扫关注上方公众号,拉学习群和答疑解惑

  • 本文作者: 图雀社区
  • 本文链接: https://tuture.co/2020/08/20/5e1396b/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!