jest测试react组件
本文由特邀作者杰克·富兰克林 ( Jack Franklin)发表 。 SitePoint访客帖子旨在为您带来来自JavaScript社区的杰出作家和演讲者的引人入胜的内容。
在本文中,我们将研究如何使用Jest (Facebook维护的测试框架)来测试ReactJS组件。 我们将首先研究如何在普通JavaScript函数上使用Jest,然后再查看Jest提供的一些特定功能,这些功能专门旨在简化React应用程序的测试。 值得注意的是,Jest并不是专门针对React的:您可以使用它来测试任何JavaScript应用程序。 但是,它提供的一些功能非常方便测试用户界面,这就是为什么它非常适合React的原因。
样品申请
在测试任何东西之前,我们需要一个应用程序进行测试! 忠于Web开发传统,我构建了一个小型的todo应用程序,我们将以此为起点。 您可以在GitHub上找到它以及我们将要编写的所有测试。 如果您想使用该应用程序来体验一下它,还可以在线找到实时演示 。
该应用程序以ES2015编写,并使用Webpack以及Babel ES2015和React预设进行编译。 我不会详细介绍构建设置,但是如果您想查看的话,所有内容都在GitHub存储库中 。 您可以在自述文件中找到有关如何使应用程序在本地运行的完整说明。 如果您想了解更多信息,请使用Webpack构建该应用程序,并且我建议使用“ Webpack 入门指南 ”作为对该工具的很好介绍。
该应用程序的入口点是app/index.js
,它仅将Todos
组件呈现为HTML:
render(
<Todos />,
document.getElementById('app')
);
Todos
组件是应用程序的主要中心。 它包含所有状态(此应用程序的硬编码数据,实际上可能来自API或类似数据),并具有呈现两个子组件的代码: Todo
,该状态中的每个todo都会呈现一次,和AddTodo
,后者呈现一次,并为用户提供添加新待办事项的表单。
因为Todos
组件包含所有状态,所以只要有任何更改,它都需要Todo
和AddTodo
组件来通知它。 因此,它将功能向下传递给这些组件,当某些数据发生更改时可以调用这些组件,并且Todos
可以相应地更新状态。
最后,目前,您会注意到所有业务逻辑都包含在app/state-functions.js
:
export function toggleDone(state, id) {…}
export function addTodo(state, todo) {…}
export function deleteTodo(state, id) {…}
这些都是纯函数,它们接受状态和一些数据,并返回新状态。 如果您不熟悉纯函数,则它们是仅引用给定数据且没有副作用的函数。 有关更多信息,您可以阅读我的纯函数列表中的 文章以及关于纯函数和React的SitePoint上的文章 。
如果您熟悉Redux,它们与Redux称为reducer的内容非常相似。 实际上,如果此应用程序变得更大,我会考虑迁移到Redux以使用更明确,结构化的数据方法。 但是对于这种规模的应用程序,您通常会发现本地组件状态和一些抽象功能足够了。
去TDD还是不去TDD?
有许多关于测试驱动开发的优缺点的文章,其中要求开发人员首先编写测试,然后再编写修复测试的代码。 其背后的想法是,通过首先编写测试,您必须考虑正在编写的API,它可以导致更好的设计。 对于我来说,我发现这很大程度上取决于个人喜好以及我正在测试的东西。 我发现,对于React组件,我喜欢先编写组件,然后将测试添加到最重要的功能中。 但是,如果您发现首先为组件编写测试适合您的工作流程,则应该这样做。 这里没有硬性规定。 做对您和您的团队最有利的事情。
请注意,本文将重点介绍测试前端代码。 如果您正在寻找后端方面的内容,请务必在Node.js中查看SitePoint的课程“ 测试驱动开发” 。
开玩笑
Jest于2014年首次发布,尽管最初引起了很多兴趣,但该项目暂时处于Hibernate状态,因此并未积极开展。 但是,Facebook在去年投入了资金来改善Jest,并在最近发布了一些具有令人印象深刻的变化的版本,使其值得重新考虑。 与最初的开源版本相比,Jest的唯一相似之处在于名称和徽标。 其他所有内容均已更改并重写。 如果您想了解更多相关信息,可以阅读Christoph Pojer的评论 ,他在其中讨论了项目的当前状态。
如果您对使用其他框架设置Babel,React和JSX测试感到沮丧,那么我绝对建议您尝试一下Jest。 如果您发现现有的测试设置很慢,我也强烈推荐Jest。 它会自动并行运行测试,并且其监视模式只能运行与更改后的文件相关的测试,这在您拥有大量测试时非常宝贵。 它配置了JSm ,这意味着您可以编写浏览器测试,但可以通过Node运行它们,可以处理异步测试,并具有内置的模拟,间谍和存根等高级功能。
安装和配置Jest
首先,我们需要安装Jest。 因为我们也在使用Babel,所以我们将安装另外两个模块,这些模块可以使Jest和Babel很好地发挥作用:
npm install --save-dev babel-jest babel-polyfill babel-preset-es2015 babel-preset-react jest
您还需要将.babelrc
文件与Babel配置为使用所需的任何预设和插件。 示例项目已经有此文件,如下所示:
{
"presets": ["es2015", "react"]
}
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
我们不会安装任何React测试工具,因为我们不会从测试组件开始,而是测试状态功能。
Jest希望在__tests__
文件夹中找到我们的测试,该文件夹已成为JavaScript社区中流行的惯例,这是我们要坚持使用的文件夹。 如果您不__tests__
设置,那么开箱即用的Jest也支持查找任何.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
脚本设置为运行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__
文件夹的同一目录中。 您还需要安装shortid
软件包( npm install shortid --save
),它是Todo应用程序的依赖项。
我将从app/state-functions.js
导入函数开始,并设置测试的结构。 尽管Jest允许您使用describe
并且it
可以根据需要深度嵌套,但是您也可以使用test
,它通常会更好地读取。 test
只是Jest的it
函数的别名,但是有时可以使测试更易于阅读且嵌套更少。
例如,这里是我会怎么写测试与嵌套的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
将返回完成状态,然后可以对以下状态进行断言:
const startState = {
todos: [{ id: 1, done: false, name: 'Buy Milk' }]
};
const finState = toggleDone(startState, 1);
expect(finState.todos).toEqual([
{ id: 1, done: true, name: '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
在顶部import语句deleteTodo
沿toggleTodo
:
import { toggleTodo, deleteTodo } from '../app/state-functions';
这是我编写测试的方式:
test('deleteTodo deletes the todo it is given', () => {
const startState = {
todos: [{ id: 1, done: false, name: 'Buy Milk' }]
};
const finState = deleteTodo(startState, 1);
expect(finState.todos).toEqual([]);
});
测试与第一个测试相差不大:我们设置了初始状态,运行了函数,然后对完成状态进行了断言。 如果您让Jest在监视模式下运行,请注意它如何拾取并运行新测试,以及这样做的速度! 这是在编写测试时获得有关测试的即时反馈的好方法。
上面的测试还演示了测试的理想布局,即:
- 建立
- 执行被测功能
- 确定结果。
通过以这种方式安排测试,您会发现它们更易于遵循和使用。
现在我们很高兴测试状态函数,让我们继续使用React组件。
测试React组件
值得注意的是,默认情况下,我实际上鼓励您不要在React组件上编写太多测试。 任何您想进行彻底测试的内容(例如业务逻辑)都应从组件中拉出,并置于独立的功能中,就像我们之前测试的状态功能一样。 也就是说,有时测试一些React交互非常有用(例如,确保在用户单击按钮时使用正确的参数调用特定功能)。 我们将从测试我们的React组件呈现正确的数据开始,然后着眼于测试交互。 然后,我们继续快照,这是Jest的一项功能,它使测试React组件的输出更加方便。
为此,我们需要使用react-addons-test-utils
,这是一个提供用于测试React的函数的库。 我们还将安装Enzyme ,这是由AirBnB编写的包装器库,使测试React组件更加容易。 在整个测试中,我们将使用此API。 Enzyme是一个很棒的库,React团队甚至推荐它为测试React组件的方法。
npm install --save-dev react-addons-test-utils enzyme
让我们测试一下Todo
组件在一个段落中呈现其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的快照功能来简化此过程。
同时,让我们看一下如何使用Jest的间谍功能来断言使用特定参数调用函数。 这对于我们的情况很有用,因为我们有Todo
组件,该组件具有两个作为属性的函数,当用户单击按钮或执行交互时应调用该函数。
在此测试中,我们将断言当单击todo时,组件将调用给定的doneChange
。
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 = TestUtils.findRenderedDOMComponentWithClass(rendered, 'toggle-todo');
然后我们可以在其上调用simulate
来模拟用户事件,将click
作为参数传递:
p.simulate('click');
剩下要做的就是断言我们的间谍函数已经正确调用。 在这种情况下,我们期望使用todo的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);
});
使用快照更好地进行组件测试
上面我提到过,测试React组件,尤其是一些更普通的功能(例如渲染文本),可能需要大量工作。 Jest允许您运行快照测试,而不是对React组件进行大量声明。 这些对于交互并不是很有用(在这种情况下,我仍然喜欢上面我们刚刚写过的测试),但是对于测试组件输出是否正确,它们要容易得多。
当您运行快照测试时,Jest会渲染被测的React组件并将结果存储在JSON文件中。 每次运行测试时,Jest都会检查React组件是否仍呈现与快照相同的输出。 然后,当您更改组件的行为时,Jest会告诉您,并且:
- 您会发现自己犯了一个错误,可以修复该组件,使其再次与快照匹配
- 或者,您有意进行了更改,则可以告诉Jest更新快照。
这种测试方式意味着:
- 您不必编写很多断言即可确保您的React组件的行为符合预期
- 您永远都不会意外更改组件的行为,因为Jest会意识到。
您也不必快照所有组件。 实际上,我会积极建议您反对。 您应该选择具有某些功能的组件,这些功能确实需要确保能正常工作。 对所有组件进行快照会导致缓慢的测试,这些测试没有用。 请记住,React是一个经过全面测试的框架,因此我们可以确信它会按预期运行。 确保您最终不要测试框架,而不是代码!
要开始进行快照测试,我们还需要一个Node软件包。 react-test-renderer是一个能够接收React组件并将其呈现为纯JavaScript对象的程序包。 这意味着可以将其保存到文件中,这就是Jest用来跟踪快照的方式。
npm install --save-dev react-test-renderer
现在,让我们重写我们的第一个Todo组件测试以使用快照。 现在, TodoComponent calls doneChange when todo is clicked
测试一下,注释掉TodoComponent calls doneChange when todo is clicked
。
您需要做的第一件事是导入react-test-renderer
,并删除mount
的导入。 它们不能同时使用。 您要么必须使用另一个。 这就是为什么我们暂时将另一项测试注释掉的原因。
import renderer from 'react-test-renderer';
现在,我将使用刚刚导入的渲染器来渲染组件,并断言它与快照匹配:
describe('Todo component renders the todo correctly', () => {
it('renders correctly', () => {
const todo = { id: 1, done: false, name: 'Buy Milk' };
const rendered = renderer.create(
<Todo todo={todo} />
);
expect(rendered.toJSON()).toMatchSnapshot();
});
});
第一次运行此命令时,Jest很聪明,意识到该组件没有快照,因此创建了快照。 让我们看一下__tests__/__snapshots__/todo.test.js.snap
:
exports[`Todo component renders the todo correctly renders correctly 1`] = `
<div
className="todo todo-1">
<p
className="toggle-todo"
onClick={[Function]}>
Buy Milk
</p>
<a
className="delete-todo"
href="#"
onClick={[Function]}>
Delete
</a>
</div>
`;
您可以看到Jest为我们保存了输出,现在,下次我们运行此测试时,它将检查输出是否相同。 为了演示这一点,我将通过删除呈现待办事项文本的段落来破坏该组件,这意味着我已经从Todo
组件中删除了以下行:
<p className="toggle-todo" onClick={() => this.toggleDone() }>{ todo.name }</p>
让我们看看杰斯特现在在说什么:
FAIL __tests__/todo.test.js
● Todo component renders the todo correctly › renders correctly
expect(value).toMatchSnapshot()
Received value does not match stored snapshot 1.
- Snapshot
+ Received
<div
className="todo todo-1">
- <p
- className="toggle-todo"
- onClick={[Function]}>
- Buy Milk
- </p>
<a
className="delete-todo"
href="#"
onClick={[Function]}>
Delete
</a>
</div>
at Object.<anonymous> (__tests__/todo.test.js:21:31)
at process._tickCallback (internal/process/next_tick.js:103:7)
Jest意识到快照与新组件不匹配,并在输出中告知我们。 如果我们认为此更改是正确的,则可以使用-u
标志运行jest,它将更新快照。 不过,在这种情况下,我将撤消更改,而Jest再次感到高兴。
接下来,我们看一下如何使用快照测试来测试交互。 每个测试可以有多个快照,因此可以测试交互后的输出是否符合预期。
我们实际上无法通过Jest快照测试Todo组件的交互,因为它们不控制自己的状态,而是调用给定的回调道具。 我在这里所做的是将快照测试移至新文件todo.snapshot.test.js中 ,并将切换测试保留在todo.test.js中 。 我发现将快照测试分成另一个文件很有用; 这也意味着您不会在react-test-renderer
和react-addons-test-utils
之间react-test-renderer
冲突。
记住,您会在GitHub上找到我在本教程中编写的所有代码,以供您检出并在本地运行。
结论
Facebook很久以前就发布了Jest,但最近它被大量使用并投入使用。 它Swift成为JavaScript开发人员的最爱,而且只会越来越好。 如果您过去曾经尝试过Jest并且不喜欢它,那么我不能鼓励您再尝试一次,因为现在它实际上是一个不同的框架。 它运行Swift,擅长重新运行规范,可提供出色的错误消息,并通过其快照功能使其一臂之力。
如果您有任何疑问,请随时在GitHub上提出问题,我们将竭诚为您服务。 并且请确保在GitHub上查看Jest并为项目加注星标; 它对维护者有帮助。
本文由Dan Prince和Christoph Pojer进行同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!
jest测试react组件