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

趣谈前端

共 9997字,需浏览 20分钟

 ·

2021-03-24 17:16

你或许早已经知道“单元测试”“端到端测试”这些名词,但从未真正付诸实践。在这一系列实战教程中,我们将手把手带你掌握 Jest、Enzyme、Cypress 等测试利器,帮助我们从 bug 的沼泽中挣脱出来,成为一个无往不利的高阶前端开发者!

本篇教程是 JavaScript 测试系列实战 的第一篇教程,首先介绍了测试的类型,然后主要通过一个 React 项目教会你如何使用 Jest 编写第一个测试,然后使用 Enzyme 对 React 组件进行浅层渲染,以方便对不同层次的组件进行细粒度测试,当学习了这篇教程之后,你将对基础的测试编写、组件的测试有一个比较好的了解。

初识 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(63)).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({ adapternew 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';
import 'jest-enzyme';

configure({ adapternew 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 数据,不见不散哦!



喜欢本文,点个“在看”告诉我

浏览 41
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报