首先,你需要知道Testing Library 包含多个库,帮助你模拟用户操作来测试UI组件。
照片由Ferenc Almasi在Unsplash拍摄。
Testing Library 并不是专门为 React 设计的,但在 React 社区中很受欢迎并且越来越受到关注。
作为一般规则,最好别用这个测试库来测试,
- 组件内部的状态
- 组件内部的方法
- 组件生命周期中的方法
- 子组件
我们为什么要用 Jest 和 Testing Library 呢?
由于Testing Library不是一个测试框架,所以你需要用到Jest(或者其他框架比如Mocha)来收集并适当地运行测试文件。
换句话说,Jest 会确保,当你运行 npm run test
时,所有的测试文件都会被运行,并且结果会显示在终端里。
如果你使用的是 React v16 或更高版本,首先安装 React 的测试库:
# 以下为安装命令,保持原样即可。
npm install --save @testing-library/react
npm install --save-dev @testing-library/react @testing-library/dom # 安装 React 和 DOM 的测试库
假设一个初始的React应用如这样的所示
// src/App.js
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
编辑 <code>src/App.js</code> 并保存来重新加载。
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
学习 React 了解更多
</a>
</header>
</div>
);
}
有两种开始写测试的方法:
- 将测试文件
App.test.js
放在它的源文件(如App.js
)附近。 - 创建一个
src/__tests__
文件夹,并将所有测试文件放进去。
无论如何,这样做比较好:将测试文件命名为源文件,并在其后添加 .test.[扩展名]
。扩展名可以为 js
, jsx
, ts
或 tsx
,
我会按照第一个约定行事。
// 从'@testing-library/react'导入render和screen
import { render, screen } from '@testing-library/react';
import App from './App';
test('显示学习React的链接', () => {
render(<App />);
const linkElement = screen.getByText(/学习React/i);
expect(linkElement).toBeInTheDocument();
});
下面的代码是用于测试React应用的一个案例,结构如下:
test
函数定义了一个名为“renders learn react link”
的测试用例。render(<App />);
这一行渲染了App
组件以进行测试。它基本上创建了一个 DOM 表示的副本。const linkElement = screen.getByText(/learn react/i);
这一行使用getByText
来查找匹配正则表达式/learn react/i
的元素。我们知道目前页面上只有一个这样的元素。- 最后,通过使用
expect(linkElement).toBeInTheDocument();
来测试该元素是否在文档中。语法中的最后部分是一个匹配器,用于检查某个内容,在这种情况下是.toBeInTheDocument()
。你可以在这里找到更多关于 Jest 匹配器 和 RTL 匹配器 的信息,了解更多详情。
这是一个使用RTL的基础测试。
瑞典测试一个简单的表单用 React Testing Library 测试一个简单的表单
让我们在 src/components/UserForm.js
创建一个简单的用户表单,然后在 App.js
里引入它。
// src/components/UserForm.js
import React, { useState } from "react";
const UserForm = ({ onUserAdd }) => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
onUserAdd({ name, email });
setName("");
setEmail("");
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
姓名:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
placeholder="请输入姓名"
/>
</label>
</div>
<div>
<label>
邮箱:
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="请输入邮箱"
/>
</label>
</div>
<button type="submit">提交</button>
</form>
);
};
export default UserForm;
正如前面所说,我们会在源代码文件附近创建一个名为 UserForm.test.js
的测试文件,比如 src/components/UserForm.test.js
。
我们想要确保有两个带有正确标签的字段,并且点击提交会触发 onUserAdd
函数。
我们从导入必要的库和工具以及需要测试的组件开始。
// UserForm.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import UserForm from "./UserForm";
最简单来说,我们可以写一个测试来验证表单上有两个带有正确标签的字段,如下。
// UserForm.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import UserForm from "./UserForm";
const mockOnUserAdd = jest.fn(); // 模拟用户添加函数
test("进行测试渲染两个带有正确标签的输入框,且每个输入框都有相应的类型", () => {
render(<UserForm onUserAdd={mockOnUserAdd} />);
const nameInput = screen.getByLabelText("Name:"); // 获取名称输入框
expect(nameInput).toBeVisible(); // 验证名称输入框可见
expect(nameInput).toHaveAttribute("type", "text"); // 验证名称输入框具有属性类型为"text"
const emailInput = screen.getByLabelText(/Email:/i); // 获取电子邮件输入框,进行不区分大小写的搜索
expect(emailInput).toBeVisible(); // 验证电子邮件输入框可见
expect(emailInput).toHaveAttribute("type", "email"); // 验证电子邮件输入框具有属性类型为"email"
});
因为我们将 onUserAdd
作为属性传递,我们需要模拟实现该函数以便能够单独测试组件。只需这样做:const mockOnUserAdd = jest.fn();
。
在测试中,我们使用 render
函数将 React 组件渲染到虚拟 DOM 中,这是测试前必不可少的一步。在测试环境中安排组件,并对其操作和断言结果也需要这一步。
由于我们将函数传递给组件,因此我们需要传递模拟函数来模仿真实函数的行为,并断言它是否被调用,被调用的次数和参数等(稍后再说)。
我们还使用 React Testing Library 提供的 screen
工具,查询新组件在用户操作时的表现(在虚拟 DOM 内)。
通过使用代码 screen.getByLabelText('Name:')
,RTL 会寻找带有用户可见标签文本 'Name:' 的元素。
我们希望输入位于文档中,并且也期望该元素具有正确的类型,即 text
和 email
。分别是文本类型 text
和电子邮件类型 email
。
最好你应该经常跑测试,使用TDD或者使用npm run test -a
来监控测试,这样每次相关文件有改动时,测试套件都会自动运行。
我们来试试点击提交按钮吧。
由于我们已经在之前的测试中定义了模拟的函数,我们可以像之前那样渲染UserForm组件。
我们依然获取名字和电子邮件输入,就像之前一样。我们使用 screen.getByRole('button', { name: 'Submit' });
来获取带有“Submit”文本的按钮元素。
// UserForm.test.js
...
test("在点击提交按钮时调用 onUserAdd", () => {
render(<UserForm onUserAdd={mockOnUserAdd} />);
const nameInput = screen.getByLabelText(/Name:/i);
const emailInput = screen.getByLabelText(/Email:/i);
const submitButton = screen.getByRole("button", { name: "Submit" });
fireEvent.change(nameInput, { target: { value: "John Doe" } });
fireEvent.change(emailInput, { target: { value: "john.doe@example.com" } });
fireEvent.click(submitButton);
expect(mockOnUserAdd).toHaveBeenCalledTimes(1); // 期望 mockOnUserAdd 被调用一次
});
我们使用 React Testing Library 的 fireEvent
来模拟浏览器中可能会发生的实际事件,像点击按钮这样的操作。记得在引入时加上 fireEvent
。
然后,fireEvent.change
模拟 DOM 的变化。在上述示例中,我们选择一个输入元素并更改其值。
最后,我们用 fireEvent.click(submitButton);
来模拟点击按钮。
最后一行,expect(mockOnUserAdd).toHaveBeenCalledTimes(1);
断言模拟的函数被调用了一次。这可能行得通,但最好让它异步,因为点击事件不是严格的同步事件。
换句话说,事件API是同步的,因为事件触发是同步的。但是,与之相反,比如点击事件等DOM事件是异步的,因为它们由外部触发,例如用户点击。
因此,我们应该这样添加 async/await
(这并不是很好,我们之后会进一步讨论一下)。
// UserForm.test.js
...
test("点击提交按钮时,调用onUserAdd", async () => {
// 准备
render(<UserForm onUserAdd={mockOnUserAdd} />);
const nameInput = screen.getByLabelText(/Name:/i);
const emailInput = screen.getByLabelText(/Email:/i);
const submitButton = screen.getByRole("button");
// 操作
await fireEvent.change(nameInput, { target: { value: 'John Doe' } }); // 更改姓名输入框的值为 'John Doe'
await fireEvent.change(emailInput, { target: { value: 'john.doe@example.com' } }); // 更改电子邮件输入框的值为 'john.doe@example.com'
await fireEvent.click(submitButton);
// 验证
expect(mockOnUserAdd).toHaveBeenCalledTimes(1);
});
请注意,我们尽量按照 Arrange、Act、Assert 方法。
- Arrange : 准备好要测试的组件
- Act : 处理 DOM 中的元素
- Assert : 验证预期
根据上面的测试结果,有几点好的做法值得大家注意。
所以你应该确保每次测试都能独立运行并重新开始,确保它们互不影响。应该在每次测试前后清除所有模拟,如下。
// 这是一个用户表单的测试文件
// UserForm.test.js
import React from "react"; // 导入React库
import { render, screen } from "@testing-library/react"; // 导入测试库的render和screen函数
import UserForm from "./UserForm";
beforeEach(() => {
mockOnUserAdd.mockClear(); // 清除mockOnUserAdd的模拟
});
afterEach(() => {
jest.clearAllMocks(); // 清除所有的模拟
});
...
有些操作可能会重复,可能最好保持DRY(DRY代表不要重复自己,Don't Repeat Yourself)。
我们可以在 afterEach
之下定义一个函数,例如:const getName = () => screen.getByLabelText(/Name:/i);
,然后在需要获取元素时调用它:const nameInput = getName();
这里有一些小建议,更多关于RTL(从右到左)的内容将会陆续更新。
在我的下一篇文章中,我将把 fireEvent
更改为 userEvent
。通常推荐使用 userEvent
,因为它有助于编写更贴近用户实际操作的测试,这使得测试更接近实际用户行为,因此更加强大和可靠。
相关链接:Jest matchers 和 RTL matchers.
React 测试库 (RTL) — 第 2 部分: 快速入门和 userEventlevelup.gitconnected.com React Testing Library (RTL) 实践技巧 — 第 3 部分 (Part 3) 使用 React Testing Library (RTL) Playground 可以帮助你更容易地理解你正在编写的测试。共同学习,写下你的评论
评论加载中...
作者其他优质文章