在本文中,我们将研究如何使用Jest(Facebook维护的测试框架)来测试React组件。我们将首先研究如何在普通JavaScript函数上使用Jest,然后再查看Jest提供的一些特定功能,这些功能专门旨在简化React应用程序的测试。
值得注意的是,Jest并不是专门针对React的:您可以使用它来测试任何JavaScript应用程序。但是,它提供的一些功能对于测试用户界面非常方便,这就是为什么它非常适合React的原因。
DEMO
在测试任何东西之前,我们需要一个应用程序进行测试!忠于Web开发传统,我构建了一个小型的todo应用程序,我们将以此为起点。您可以在GitHub上找到它以及我们将要编写的所有测试。如果您想使用该应用程序来体验一下它,还可以在线找到实时演示[https://sitepoint-editors.github.io/testing-react-with-jest]。
该应用程序以ES2015编写,并使用带有Babel ES2015和React预设的webpack进行编译。我不会介绍构建设置的详细信息,但是如果您想查看的话,所有内容都在GitHub存储库中。您可以在自述文件中找到有关如何使应用程序在本地运行的完整说明。如果您想了解更多信息,请使用webpack构建该应用程序,并且我建议使用“ webpack入门指南”作为对该工具的很好介绍。
应用程序的入口是app/index.js
,它只是将Todos
组件呈现为HTML:
render( <Todos />, document.getElementById('app') );
该Todos
组件是应用程序的主要中心。它包含所有状态(此应用程序的硬编码数据,实际上可能来自API或类似数据),并具有呈现两个子组件的代码:Todo
,对于该状态中的每个待办事项都呈现一次;以及AddTodo
,该渲染一次,并为用户提供添加新待办事项的表单。
由于Todos
组件包含所有状态,因此,只要有任何更改,它都需要Todo
和AddTodo
组件来通知它。因此,它将功能向下传递到这些组件中,这些组件可以在某些数据更改时调用,并Todos
可以相应地更新状态。
最后,现在,您会注意到所有业务逻辑都包含在app/state-functions.js
:
export function toggleDone(todos, id) {…} export function addTodo(todos, todo) {…} export function deleteTodo(todos, id) {…}
这些都是纯函数,它们带有状态(对于我们的示例应用程序,是一个待办事项数组)和一些数据,并返回新状态。如果您不熟悉纯函数,那么它们就是仅引用给定数据且没有副作用的函数。
如果您熟悉Redux,它们与Redux称为reducer的内容非常相似。实际上,如果此应用程序变得更大,我会考虑迁移到Redux以使用更明确,结构化的数据方法。但是对于如此大的应用程序,您常常会发现本地组件状态和一些抽象功能足够了。
去TDD还是不去TDD?
已经有很多关于测试驱动开发的优缺点的文章,其中要求开发人员先编写测试,然后再编写修复测试的代码。其背后的想法是,通过首先编写测试,您必须考虑正在编写的API,它可以导致更好的设计。我发现这很大程度上取决于个人喜好以及我正在测试的某种东西。我发现,对于React组件,我喜欢先编写组件,然后将测试添加到最重要的功能中。但是,如果您发现首先为组件编写测试适合您的工作流程,则应该这样做。这里没有硬性规定。做对您和您的团队最有利的事情。
Jest介绍
Jest于2014年首次发布,尽管最初引起了人们的极大兴趣,但该项目处于休眠状态一段时间,因此并未积极进行。但是,Facebook在改进Jest方面投入了很多精力,并且最近发布了一些具有令人印象深刻的更改的版本,使其值得重新考虑。与最初的开源版本相比,Jest的唯一相似之处在于名称和徽标。其他所有内容均已更改并重写。
如果您对使用其他框架设置Babel,React和JSX测试感到沮丧,那么我绝对建议您尝试一下Jest。如果您发现现有的测试设置很慢,我也强烈推荐Jest。它会自动并行运行测试,并且其监视模式只能运行与更改后的文件相关的测试,这在您拥有大量测试时非常宝贵。它带有配置的JSm,这意味着您可以编写浏览器测试,但可以通过Node运行它们。它可以处理异步测试,并具有内置的模拟,间谍和存根等高级功能。
安装和配置Jest
首先,我们需要安装Jest。因为我们也在使用Babel,所以我们将安装另外两个模块,这些模块使Jest和Babel以及Babel和所需的预设都能很好地发挥作用:
npm install --save-dev jest babel-jest @babel/core @babel/preset-env @babel/preset-react
您还需要将babel.config.js
Babel配置为使用所需的任何预设和插件的文件。示例项目已经有此文件,如下所示:
module.exports = { presets: [ '@babel/preset-env', '@babel/preset-react', ], };
本文不会深入介绍如何设置Babel。
我们不会安装任何React测试工具,因为我们不会先测试组件,而是测试状态功能。
Jest希望在一个__tests__
文件夹中找到我们的测试,该文件夹已成为JavaScript社区中流行的惯例,这是我们要坚持使用的文件夹。如果你不是一个风扇__tests__
安装,开箱即用的玩笑也支持找到任何.test.js
和.spec.js
文件了。
在测试状态函数时,请继续创建__tests__/state-functions.test.js
。
我们将很快编写一个适当的测试,但是现在,放入此虚拟测试,这将使我们检查所有工作是否正常,并配置了Jest:
describe('Addition', () => { it('knows that 2 and 2 make 4', () => { expect(2 + 2).toBe(4); }); });
现在,进入您的package.json
。我们需要进行设置npm test
,使其运行Jest,我们只需将test
脚本设置为run就可以做到这一点jest
:
"scripts": { "test": "jest" }
如果您现在在npm test
本地运行,则应该看到测试运行并通过!
PASS __tests__/state-functions.test.js Addition knows that 2 and 2 make 4 (5ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 passed, 0 total Time: 3.11s
如果您曾经使用过Jasmine或大多数测试框架,则上面的测试代码本身应该非常熟悉。Jest让我们根据需要使用describe
和it
嵌套测试。您使用多少嵌套取决于您。我喜欢嵌套我的,所以所有描述性字符串都传递给我describe
,it
几乎就像一个句子一样阅读。
在进行实际的断言时,您可以在expect()
调用之前将要测试的内容包装在一个调用中。在这种情况下,我们使用了toBe
。您可以在Jest文档中找到所有可用断言的列表。toBe
检查给定值是否与被测值匹配,并===
以此来进行测试。通过本教程,我们将满足Jest的一些断言。
测试业务逻辑
既然我们已经看到了Jest在虚拟测试上的工作,那就让它在真实的测试上运行!我们将测试第一个状态函数toggleDone
。toggleDone
获取当前状态和我们想要切换的待办事项ID。每个待办事项都有一个done
属性,toggleDone
应将其从交换true
到false
,反之亦然。
注意:如果要遵循此步骤,请确保已克隆存储库并将app
文件夹复制到包含___tests__
文件夹的相同目录中。您还需要安装所有应用程序的依赖项(例如React)。npm install
克隆存储库后,可以通过运行来确保已全部安装。
我将从导入函数app/state-functions.js
并设置测试的结构开始。虽然Jest允许您随意使用describe
和it
嵌套,但也可以使用test
,它通常会更好地读取。test
只是Jestit
函数的别名,但有时可以使测试更易于阅读且嵌套更少。
例如,这是我如何使用嵌套describe
和it
调用编写该测试的方法:
import { toggleDone } from '../app/state-functions'; describe('toggleDone', () => { describe('when given an incomplete todo', () => { it('marks the todo as completed', () => { }); }); });
这就是我的处理方式test
:
import { toggleDone } from '../app/state-functions'; test('toggleDone completes an incomplete todo', () => { });
测试仍然可以很好地阅读,但是现在缩进的方式越来越少了。这主要取决于个人喜好;选择您更喜欢的风格。
现在我们可以写断言了。首先,我们将创建初始状态,然后将其传递到toggleDone
中以及要切换的待办事项的ID。toggleDone
将返回完成状态,然后可以对以下状态进行断言:
import { toggleDone } from "../app/state-functions"; test("tooggleDone completes an incomplete todo", () => { const startState = [{ id: 1, done: false, text: "Buy Milk" }]; const finState = toggleDone(startState, 1); expect(finState).toEqual([{ id: 1, done: true, text: "Buy Milk" }]); });
现在注意,我常使用toEqual
我的断言。您应该toBe
在原始值(例如字符串和数字)上使用,但toEqual
在对象和数组上使用。toEqual
用于处理数组和对象,并且将递归检查给定对象中的每个字段或项以确保它们匹配。
这样,我们现在可以运行npm test
并查看状态函数测试通过:
PASS __tests__/state-functions.test.js tooggleDone completes an incomplete todo (9ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 passed, 0 total Time: 3.166s
重新测试变更
对测试文件进行更改,然后不得不npm test
再次手动运行,这有点令人沮丧。Jest的最佳功能之一是其监视模式,该模式监视文件更改并相应地运行测试。它甚至可以根据更改的文件找出要运行的测试子集。它功能强大且可靠,您可以在监视模式下运行Jest,并在编写代码的过程中将其整日保留。
要以监视模式运行它,可以运行npm test -- --watch
。npm test
在第一个--
命令之后传递给您的所有内容都将直接传递给基础命令。这意味着这两个命令实际上是等效的:
-
npm test -- --watch
-
jest --watch
我建议您在本教程的其余部分中让Jest在另一个选项卡或终端窗口中运行。
在继续测试React组件之前,我们将在另一个状态函数上再编写一个测试。在实际的应用程序中,我将编写更多的测试,但是出于教程的考虑,我将跳过其中的一些测试。现在,让我们编写一个测试来确保我们的deleteTodo
功能正常工作。在查看下面的内容之前,请尝试自己编写并查看测试结果。
请记住,您必须更新import
顶部的语句才能deleteTodo
与一起导入toggleTodo
:
import { toggleDone, deleteTodo } from "../app/state-functions";
这是我编写测试的方式:
test('deleteTodo deletes the todo it is given', () => { const startState = [{ id: 1, done: false, text: 'Buy Milk' }]; const finState = deleteTodo(startState, 1); expect(finState).toEqual([]); });
测试与第一个测试相差不大:我们设置了初始状态,运行了函数,然后对完成状态进行了断言。如果您让Jest在监视模式下运行,请注意它如何拾取和运行新测试,以及这样做的速度!这是在编写测试时获得有关测试的即时反馈的好方法。
上面的测试还演示了测试的理想布局,即:
-
设定
-
执行被测功能
-
确定结果
通过以这种方式安排测试,您会发现它们更易于遵循和使用。
现在我们很高兴测试状态函数,让我们继续研究React组件。
测试React组件
值得注意的是,默认情况下,我实际上鼓励您不要在React组件上编写太多测试。任何您要进行彻底测试的内容(例如业务逻辑)都应从组件中拉出,并置于独立功能中,就像我们之前测试的状态功能一样。也就是说,有时测试一些React交互很有用(例如,确保当用户单击按钮时使用正确的参数调用特定功能)。我们将从测试我们的React组件呈现正确的数据开始,然后着眼于测试交互。
为了编写我们的测试,我们将安装Enzyme,这是由Airbnb编写的包装器库,它使测试React组件更加容易。
注意:自从本文是第一篇文章以来,React团队不再使用Enzyme,而是推荐使用React Testing Library(RTL)。值得阅读该页面。如果您要维护已经进行了酶测试的代码库,则无需删除所有内容并迁移,但是对于新项目,建议您考虑使用RTL。
与酶一起,我们还需要为所使用的任何版本的React安装适配器。对于React v16,应该是enzyme-adapter-react-16
,但是对于React v17,目前没有可用的官方适配器,因此我们必须使用非官方版本。请注意,在正式支持发布之前,此软件包仅供参考,届时将不推荐使用。
您可以在此GitHub问题中关注正式版本的进度。
npm install --save-dev enzyme @wojtekmaj/enzyme-adapter-react-17
酶需要少量的设置。在项目的根目录中,创建setup-tests.js以下代码并将其放在其中:
import { configure } from 'enzyme'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; configure({ adapter: new Adapter() });
然后,在执行任何测试之前,我们需要告诉Jest为我们运行该文件。我们可以通过配置setupFilesAfterEnv选项来做到这一点。您可以将Jest配置放入其自己的文件中,但是我喜欢使用package.json并将东西放置在一个jest对象中,Jest还将使用它:
"jest": { "setupFilesAfterEnv": [ "./setup-tests.js" ] }
现在我们准备编写一些测试!让我们测试一下该Todo组件是否在一个段落中呈现了其待办事项的文本。首先,我们将创建__tests__/todo.test.js,并导入我们的组件:
import Todo from '../app/todo'; import React from 'react'; import { mount } from 'enzyme'; test('Todo component renders the text of the todo', () => { });
我也mount从酶导入。该mount函数用于呈现我们的组件,然后允许我们检查输出并对其进行断言。即使我们在Node中运行测试,我们仍然可以编写需要DOM的测试。这是因为Jest配置了jsdom,这是一个在Node中实现DOM的库。这很棒,因为我们可以编写基于DOM的测试,而不必每次都启动浏览器进行测试。
我们可以mount用来创建我们的Todo:
const todo = { id: 1, done: false, name: 'Buy Milk' }; const wrapper = mount( <Todo todo={todo} /> );
然后我们可以调用wrapper.find,为它提供一个CSS选择器,以查找我们期望包含Todo文本的段落。该API可能会让您想起jQuery,这是设计使然。这是一个非常直观的API,用于搜索渲染的输出以找到匹配的元素。
const p = wrapper.find('.toggle-todo');
最后,我们可以断言其中的文本是Buy Milk:
expect(p.text()).toBe('Buy Milk');
整个测试看起来像这样:
import Todo from '../app/todo'; import React from 'react'; import { mount } from 'enzyme'; test('TodoComponent renders the text inside it', () => { const todo = { id: 1, done: false, name: 'Buy Milk' }; const wrapper = mount( <Todo todo={todo} /> ); const p = wrapper.find('.toggle-todo'); expect(p.text()).toBe('Buy Milk'); });
现在,我们进行了一项测试,以检查是否可以成功渲染待办事项。
接下来,让我们看看如何使用Jest的间谍功能来断言使用特定参数调用函数。在我们的案例中这很有用,因为我们拥有Todo赋予了两个功能的组件作为属性,当用户单击按钮或执行交互时应调用该组件。
在此测试中,我们将断言当单击todo时,组件将调用doneChange其给定的prop:
test('Todo calls doneChange when todo is clicked', () => { });
我们想要一个可以用来跟踪其调用以及调用其参数的函数。然后,我们可以检查是否在用户单击待办事项时doneChange调用了该函数,并且还调用了正确的参数。值得庆幸的是,Jest开箱即用地提供了间谍程序。一个间谍是一个函数,它的实现你不在乎; 您只需关心何时以及如何调用它。当您监视功能时,请考虑一下它。要创建一个,我们调用jest.fn():
const doneChange = jest.fn();
这提供了我们可以监视并确保正确调用的功能。让我们从渲染Todo正确的道具开始:
const todo = { id: 1, done: false, name: 'Buy Milk' }; const doneChange = jest.fn(); const wrapper = mount( <Todo todo={todo} doneChange={doneChange} /> );
接下来,我们可以再次找到我们的段落,就像之前的测试一样:
const p = wrapper.find(“.toggle-todo”);
然后我们可以调用simulate它来模拟用户事件,并click作为参数传递:
p.simulate(‘click’);
剩下要做的就是断言我们的间谍函数已经正确调用。在这种情况下,我们希望使用待办事项的ID调用它1。我们可以使用它expect(doneChange).toBeCalledWith(1)来断言-这样,我们就完成了测试!
test('TodoComponent calls doneChange when todo is clicked', () => { const todo = { id: 1, done: false, name: 'Buy Milk' }; const doneChange = jest.fn(); const wrapper = mount( <Todo todo={todo} doneChange={doneChange} /> ); const p = wrapper.find('.toggle-todo'); p.simulate('click'); expect(doneChange).toBeCalledWith(1); });
结论
Facebook很久以前就发布了Jest,但最近它被大量使用并大量使用。它已迅速成为JavaScript开发人员的最爱,而且只会越来越好。如果您过去曾经尝试过Jest并且不喜欢它,那么我不能鼓励您再尝试一次,因为现在它实际上是一个不同的框架。它速度快,擅长重新运行规范,提供出色的错误消息,并具有用于编写良好测试的出色表达API。