我可以猜到你在想什么:另一个 React 测试库?CSS-Tricks 上已经介绍过很多了(哎呀,我之前已经发布过一篇 介绍 Jest 和 Enzyme 的文章),难道选项还不够多吗?
但是 react-testing-library 不仅仅是另一个测试库。它是一个测试库,没错,但它建立在一个将它与其他库区分开来的基本原则之上。
您的测试越类似于软件的使用方式,它们就能为您提供越大的信心。
它试图解决用户如何使用您的应用程序的测试。事实上,它的设计方式使得即使您重构组件,测试也不会中断。我知道我们在 React 之旅的某个时刻都遇到过这种情况。
我们将花一些时间一起使用 react-testing-library 为我构建的一个简单的待办事项应用程序编写测试。您可以克隆 该仓库 到本地。
git clone https://github.com/kinsomicrote/todoapp-test.git
如果您这样做了,接下来安装所需的软件包。
## yarn
yarn add --dev react-testing-library jest-dom
## npm
npm install --save-dev react-testing-library jest-dom
如果您想知道为什么 Jest 在那里,我们正在使用它进行断言。在 src
目录中创建一个名为 __test__
的文件夹,并创建一个名为 App.test.js
的新文件。
获取快照
快照测试会记录对已测试组件执行的测试,以便以可视化的方式查看更改之间发生了哪些变化。
当我们第一次运行此测试时,我们会获取组件外观的第一个快照。因此,第一个测试注定会通过,因为,嗯,没有其他快照可以与之比较,这会表明某些内容失败了。只有当我们通过添加新元素、类、组件或文本来对组件进行新的更改时,它才会失败。添加在创建快照或上次更新时不存在的内容。
快照测试将是我们在这里编写的第一个测试。让我们打开 App.test.js
文件并使其如下所示
import React from 'react';
import { render, cleanup } from "react-testing-library";
import "jest-dom/extend-expect";
import App from './App';
afterEach(cleanup);
it("matches snapshot", () => {
const { asFragment } = render(<App />);
expect(asFragment()).toMatchSnapshot();
});
这将导入我们用于编写和运行测试的必要软件包。render
用于显示我们要测试的组件。我们使用 cleanup
在每次测试运行后清除内容——正如您在 afterEach(cleanup)
行中看到的那样。
使用 asFragment
,我们获取渲染组件的 DocumentFragment
。然后我们期望它与已创建的快照匹配。
让我们运行测试看看会发生什么。
## yarn
yarn test
## npm
npm test
正如我们现在所知,如果这是我们的第一个测试,组件的快照将在 __tests__
目录中名为 __snapshots__
的新文件夹中创建。我们实际上会在其中获得一个名为 App.test.js.snap
的文件,它看起来像这样
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`matches snapshot 1`] = `
<DocumentFragment>
<div
class="container"
>
<div
class="row"
>
<div
class="col-md-6"
>
<h2>
Add Todo
</h2>
</div>
</div>
<form>
<div
class="row"
>
<div
class="col-md-6"
>
<input
class="form-control"
data-testid="todo-input"
placeholder="Enter a task"
type="text"
value=""
/>
</div>
</div>
<div
class="row"
>
<div
class="col-md-6"
>
<button
class="btn btn-primary"
data-testid="add-task"
type="submit"
>
Add Task
</button>
</div>
</div>
</form>
<div
class="row todo-list"
>
<div
class="col-md-6"
>
<h3>
Lists
</h3>
<ul
data-testid="todos-ul"
>
<li>
<div>
Buy Milk
<button
class="btn btn-danger"
>
X
</button>
</div>
</li>
<li>
<div>
Write tutorial
<button
class="btn btn-danger"
>
X
</button>
</div>
</li>
</ul>
</div>
</div>
</div>
</DocumentFragment>
`;
现在,让我们测试 DOM 元素和事件
我们的应用程序包含两个默认情况下在应用程序首次运行时显示的待办事项。我们要确保它们实际上在第一次应用程序运行时显示,因此,要测试这一点,我们必须定位无序列表 (<ul>
) 并检查长度。我们期望长度等于二——项目的数量。
it('it displays default todo items', () => {
const { getByTestId } = render(<App />);
const todoList = getByTestId('todos-ul');
expect(todoList.children.length).toBe(2);
});
我们在该代码段中使用 getByTestId
从 App
组件中提取测试 ID。然后我们设置 todoList
以定位 todos-ul
元素。这应该是返回为二的结果。
使用我们到目前为止学到的知识,看看您是否可以编写一个测试来断言用户可以在输入字段中输入值。以下是您需要执行的操作
- 获取输入字段
- 为输入字段设置一个值
- 触发更改事件
- 断言输入字段的值与您在步骤 2 中为其设置的值相同
不要偷看下面的答案!花尽可能多的时间。
还在继续吗?太好了!我去喝杯咖啡,马上回来。
嗯,咖啡。☕️
哦,你完成了!你真棒。让我们比较一下答案。我的看起来像这样
it('allows input', () => {
const {getByTestId } = render(<App />)
let item = 'Learn React'
const todoInputElement = getByTestId('todo-input');
todoInputElement.value = item;
fireEvent.change(todoInputElement);
expect(todoInputElement.value).toBe('Learn React')
});
使用 getByTestId
,我能够提取应用程序中的测试 ID。然后我创建一个变量,将其设置为字符串 Learn React,并将其作为输入字段的值。接下来,我使用其测试 ID 获取输入字段,并在设置输入字段的值后触发更改事件。完成此操作后,我断言输入字段的值确实是 Learn React。
这与您的答案一致吗?如果您有其他方法,请留下评论!
接下来,让我们测试一下我们是否可以添加新的待办事项。我们需要获取输入字段、添加新项目的按钮和无序列表,因为这些是创建新项目所需的所有元素。
我们为输入字段设置一个值,然后触发按钮点击以添加任务。我们能够通过使用 getByText
获取按钮来做到这一点——通过在文本为 Add Task 的 DOM 元素上触发点击事件,我们应该能够添加一个新的待办事项。
让我们断言无序列表元素中的子元素(列表项)数量等于三。这假设默认任务仍然完好无损。
it('adds a new todo item', () => {
const { getByText, getByTestId } = render(<App />);
const todoInputElement = getByTestId('todo-input');
const todoList = getByTestId('todos-ul');
todoInputElement.value = 'Learn React';
fireEvent.change(todoInputElement);
fireEvent.click(getByText('Add Task'))
expect(todoList.children.length).toBe(3);
});
非常棒,对吧?
这只是在 React 中进行测试的一种方法
您可以在下一个 React 应用程序中尝试 react-testing-library。 仓库中的文档 非常详尽,并且——与大多数工具一样——是最佳的起点。 Kent C. Dodds 创建了它,并在 Frontend Masters 上开设了关于测试的完整课程(需要订阅),该课程还涵盖了 react-testing-library 的来龙去脉。
也就是说,这只是 React 的一个测试资源。当然还有其他资源,但希望这是您现在感兴趣尝试的一个资源,因为您已经了解了一些内容,但当然,请使用最适合您项目的资源。
“事实上,它的设计方式使得即使您重构组件,测试也不会中断。”
嗯……您的示例有点否认了这个论点,或者也许您提出的单元测试方法与我所知略有不同。
列表示例
如果出于某种原因我决定使用 div 而不是 ul 会怎样?我知道这不是一个好习惯,但有时你必须这样做。然后我将不得不重构我的测试……因此我更希望将 List 组件拆分为 ListWrapper 和 ListItem,并搜索 ListItem 的存在。因此,我将实现细节与业务需求分离。
输入示例
我真的很不喜欢在单元测试中使用 ID 或类名。原因如下:当您使用可重用组件时,您可能永远不应该使用 ID :) 当您使用类名作为参考时,您可能会在某个时候更改类名,并且您将不得不重构测试,仅仅因为您将“big-red-button”更改为“big-orange-button”。同样,我认为最好使用组件引用。此外,我不明白这段代码如何比 Enzyme 的
simulate
更好地模拟用户行为?以及快照测试……
我从未找到编写它们的理由。我使用单元测试来测试组件的逻辑或结构。我使用集成测试来测试组件的协作。最后,我使用 e2e 测试(少量)来测试整个页面。我找不到快照测试的位置,因为它们显然不是视觉回归测试(这可能很有用,但我不太了解)。我认为它们可能会导致大量误报。
您的观点是什么?您在现实场景中发现它们有用吗?
当您去喝咖啡时,我们进行的测试实际上并没有设置任何内容。您设置了
input.value = someVal
,然后测试了input.value === input.value
。我认为下面的方法更清晰一些按照您的说明操作时,我遇到了错误。显然,您现在应该安装/导入 @testing-library/react 和 @testing-library/jest,而不是 react-testing-library 和 jest-dom
import {render, cleanup, fireEvent} from ‘@testing-library/react’
import ‘@testing-library/jest-dom/extend-expect’