🐙单元测试🐙初探---React Testing Libary
前段时间我们项目组的架构给我们的项目添加了单元测试的功能,也要求我们开始写一些单元测试代码。这部分也是第一次接触,只能说项目组前端人多的好处就是项目里面的东西比较多,不像以前的项目,一个基于Vue2的项目做了一年多,就是做一些基本的业务功能,里面也没啥新东西。
我们项目是React的项目,单元测试也是基于@testing-library/react
进行的。本篇文章主要做一些比较基础的入门;因为实战还很少,甚至可以说没啥实战,等后面实战多了,积累多了,再进行总结和记忆吧。
话不多说,直接开整。
什么是单元测试
单元测试就像是一场考试,测试你的成绩,回想起以往的周考,月考等等,真是往事不堪回首啊。
那既然是考试,那你写的单元测试就像是试卷,而考试的对象就是你的代码,而我们这些程序员就像是老师,那些测试的代码就像是我们的学生一样。
我们可以通过单元测试,来检验这些学生的水平,看有没有出错,有没有遗漏的地方。如果学生答对了,则测试通过,反之,则测试不通过。
当然我们在出题的过程中,也要给出对应的答案,例如:
期望(学生的回答)是(标准答案) --> expect(xxx).toBe(xxx)
如果学生的回答和标准答案一致,则可以判定测试通过,反之则判定失败。
而这个出题、验证和进行判断的过程,我们可以叫做
断言
。
当然上述的讲解,只是简单的描述,单元测试肯定更加复杂的多。这里我们简单理解即可,等你用多了,可以更加深入了解。当然也是我现在理解的比较通俗吧。
React Testing Library 介绍
React Testing Library
是一个用于测试React组件的工具库,它提供了一组API,用于模拟用户与React组件的交互,并对组件的渲染结果进行断言和验证。与其他测试工具相比,React Testing Library更注重于测试的用户行为和交互,而不是组件的内部实现细节。
React Testing Library与各种测试工具(如Jest)和断言库(如React Test Renderer)兼容,并且与React生态系统紧密集成,因此在React项目中广泛应用,并且还提供了良好的文档和示例。
React Testing Library 的设计理念是从用户的角度出发,关注组件的行为和交互,而不是关注组件的内部实现细节
。这样能够更好地测试组件的行为是否符合用户预期,并且帮助开发者编写更稳定、可靠的代码。
测试流程
使用React Testing Library进行测试通常遵循以下步骤:
-
安装React Testing Library:首先,在项目中安装React Testing Library。你可以使用npm或者yarn进行安装。
npm install --save-dev @testing-library/react
或者
yarn add --dev @testing-library/react
-
编写测试用例:创建一个测试文件,通常以
.test.js
或.spec.js
结尾。在测试文件中,编写测试用例来测试React组件的行为和状态。 -
导入必要的库和组件:在测试文件中,导入React Testing Library提供的测试工具和你要测试的React组件。
import { render, screen } from '@testing-library/react'; import MyComponent from './MyComponent';
-
编写测试用例:编写测试用例来测试组件的各种行为和状态。使用React Testing Library提供的API来选择组件的DOM元素、触发事件、断言组件的行为和状态等。
test('renders component correctly', () => { render(<MyComponent />); // Use queries to select DOM elements const buttonElement = screen.getByRole('button', { name: 'Click me' }); // Assert component behavior or state expect(buttonElement).toBeInTheDocument(); });
-
运行测试:运行测试用例,确保组件按预期工作。通常使用测试运行器(如Jest)来运行测试。
npm test
或者
yarn test
-
重复步骤4和5:根据需要编写更多的测试用例来覆盖组件的不同行为和状态,确保组件的功能和可靠性。
通过这些步骤,你可以使用React Testing Library编写和运行测试,以确保React组件按预期工作并且具有正确的行为和状态。
测试用例编写步骤
创建一个与被测试组件相对应的测试文件。通常,这个文件会放在 __tests__
文件夹内或直接与组件文件放在同一目录下。文件名通常以 .test.js
或 .spec.js
结尾。
例如,如果你有一个 Button.js
组件,测试文件可以是 Button.test.js
。
-
导入必要模块
在测试文件中导入需要的库和组件。通常包括 React、被测试的组件、React Testing Library 的方法,以及 Jest 的断言库。
import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; // 为了额外的匹配器方法,如 toBeInTheDocument import Button from './Button'; // 假设 Button 组件和测试文件在同一目录
-
编写测试用例
在 React Testing Library 中,
screen
是一个全局对象,它简化了对渲染结果进行查询的过程。使用screen
可以更方便地访问和断言渲染后的 DOM 元素。screen
提供了一系列查询方法,这些方法通常是由getBy
、queryBy
和findBy
等前缀组成。使用
test
或it
方法编写测试用例。这些方法接受两个参数:测试的描述(字符串)和执行测试的函数。test('renders the button and handles click events', () => { // 渲染组件 render(<Button />); // 查找按钮元素 const buttonElement = screen.getByText('Click me'); // 断言按钮存在于文档中 expect(buttonElement).toBeInTheDocument(); // 模拟点击事件 fireEvent.click(buttonElement); // 查找消息元素 const messageElement = screen.getByText('Button clicked!'); // 断言消息存在于文档中 expect(messageElement).toBeInTheDocument(); });
-
渲染组件
使用
render
方法渲染你的 React 组件。render
方法会返回包含对渲染结果进行查询的工具。const { getByText } = render(<Button />);
-
查询元素
利用 React Testing Library 提供的查询方法如
screen.getByText
,screen.getByRole
等查找 DOM 元素。const buttonElement = screen.getByText('Click me');
-
触发事件
使用
fireEvent
或userEvent
模拟用户交互。fireEvent
是 React Testing Library 内置的方法,而userEvent
更接近真实用户行为,可以单独安装和使用。fireEvent.click(buttonElement);
-
断言结果
使用 Jest 提供的
expect
方法进行断言,验证组件的行为是否符合预期。expect(buttonElement).toBeInTheDocument(); const messageElement = screen.getByText('Button clicked!'); expect(messageElement).toBeInTheDocument();
示例
以下是完整的测试文件示例:
// Button.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Button from './Button';
test('renders the button and handles click events', () => {
// 渲染组件
render(<Button />);
// 查找按钮元素
const buttonElement = screen.getByText('Click me');
// 断言按钮存在于文档中
expect(buttonElement).toBeInTheDocument();
// 模拟点击事件
fireEvent.click(buttonElement);
// 查找消息元素
const messageElement = screen.getByText('Button clicked!');
// 断言消息存在于文档中
expect(messageElement).toBeInTheDocument();
});
React Testing Library 常用API
React Testing Library提供了一系列常用的API,用于测试React组件。
这里我们按照上一章节的测试用例的编写步骤来讲述一下��用的API。
组件渲染
在React Testing Library中,用于组件渲染的常用API是
render()
函数。这个函数用于将React组件渲染到虚拟DOM中,并返回一个对象,该对象包含了与该组件相关的查询方法。
下面是render()
函数的基本用法和示例:
import { render } from '@testing-library/react';
import MyComponent from './MyComponent';
test('renders component correctly', () => {
// 渲染组件
const { getByText } = render(<MyComponent />);
// 通过文本内容获取DOM元素
const textElement = getByText('Hello, World!');
// 断言组件是否渲染正确
expect(textElement).toBeInTheDocument();
});
在这个示例中,render()
函数用于将<MyComponent />
组件渲染到虚拟DOM中。然后,使用getByText()
方法通过文本内容获取DOM元素。最后,使用toBeInTheDocument()
断言方法检查获取到的DOM元素是否存在于DOM中。
查询元素
React Testing Library 提供了一系列强大的查询API,用于查找和验证DOM中的元素。这些查询方法分为几类,每一类都有其特定的用途和特点。
我们看一下官网总结的查询元素的表格:
Type of Query | 0 Matches | 1 Match | >1 Matches | Retry (Async/Await) |
---|---|---|---|---|
Single Element | ||||
getBy... | Throw error | Return element | Throw error | No |
queryBy... | Return null | Return element | Throw error | No |
findBy... | Throw error | Return element | Throw error | Yes |
Multiple Elements | ||||
getAllBy... | Throw error | Return array | Return array | No |
queryAllBy... | Return [] | Return array | Return array | No |
findAllBy... | Throw error | Return array | Return array | Yes |
这里我们可以看到:
查询元素有三种方法,分别是:
get
,query
和find
。通过这些方法我们可以查询对应的单个元素,如果想查找全部元素的话,只需要加All字段即可,类似于querySelect
和querySelectorAll
。
那这三种方法有什么差异呢,为什么会有这三个,难道一个不能直接通用吗?
这里我们可以根据上面的表格,来对这三个方法进行讲述:
get
方法
- 同步查询:
get
方法是同步的,立即返回匹配的元素。 - 找不到元素时抛出错误:如果没有找到匹配的元素,会抛出一个错误。
- 常用场景:适用于你确信元素应该立即存在于 DOM 中的情况。
query
方法
- 同步查询:
query
方法也是同步的,立即返回匹配的元素。 - 找不到元素时返回 null:如果没有找到匹配的元素,不会抛出错误,而是返回
null
。 - 常用场景:适用于测试某个元素不应该存在的情况。
find
方法
- 异步查询:
find
方法是异步的,返回一个 Promise。 - 等待元素出现或超时:在等待匹配的元素出现时,默认会等待一定的时间(通常是 1000ms),如果没有找到元素,会抛出一个错误。
- 常用场景:适用于异步渲染的元素,比如组件加载数据后显示的内容。
示例:
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
test('renders component with async text', async () => {
render(<MyComponent />);
const element = await screen.findByText('Async Text');
expect(element).toBeInTheDocument();
});
总结
get
方法:同步,找不到元素时抛出错误,适用于元素应立即存在的情况。query
方法:同步,找不到元素时返回null
,适用于测试元素不应该存在的情况。find
方法:异步,返回 Promise,适用于元素异步出现的情况。
其中
get
方法是最常用到的,除非测试元素不应该存在的情况,会使用query
方法,和测试元素异步出现的情况,使用find
方法,其他情况一般都用get
方法。`
All的方法,直接看表格即可,这里就不进行再次赘述了。
以下是一些常用的查询元素的 API:
getBy...
方法在传递字符串的情况下,查找规则是精准匹配,可以正则的形式进行模糊匹配。其他方法也是相同的。
getByRole
:查找具有特定角色的元素。通常用于查找按钮、链接等具有 ARIA 角色的元素。
const button = getByRole('button', { name: /submit/i });
getByText
:查找包含特定文本内容的元素。常用于查找包含特定文字的任意元素。
const linkElement = getByText(/learn react/i);
getByLabelText
:查找关联特定文本标签的表单控件。用于查找带有 <label>
标签的表单输入元素。
const input = getByLabelText(/username/i);
getByPlaceholderText
:查找具有特定占位符文本的表单控件。用于查找带有 placeholder
属性的输入元素。
const input = getByPlaceholderText(/enter your username/i);
getByAltText
:查找具有特定 alt 属性文本的图像元素。用于查找带有 alt
属性的 <img>
元素。
const image = getByAltText(/product image/i);
getByTitle
:查找具有特定 title 属性的元素。用于查找带有 title
属性的任意元素。
const element = getByTitle(/close/i);
getByTestId
:查找具有特定 data-testid
属性的元素。用于查找带有 data-testid
属性的元素,通常用于测试中特定元素的选择。
const element = getByTestId('custom-element');
这些 getBy
查询函数都是同步的,并且在找不到匹配的元素时会抛出错误,因此适合用于确保元素存在的情况。使用这些函数,可以更高效地编写可靠的测试用例。
这里只介绍常见的getBy
查询函数,其他的queryBy...
,findBy...
和与之对应的查找全部的方法,和上面的getBy
查询函数类似,只是前面的查询方法不同而已。
这里再强调一下:
其中
get
方法是最常用到的,除非测试元素不应该存在的情况,会使用query
方法,和测试元素异步出现的情况,使用find
方法,其他情况一般都用get
方法。`
触发事件
在 React Testing Library 中,触发事件通常使用
fireEvent
和userEvent
两个工具。fireEvent
是 React Testing Library 内置的,而userEvent
则是一个更高级的用户模拟工具,提供了更接近用户实际行为的事件触发方法。
fireEvent
fireEvent
是 React Testing Library 内部的一部分,是一个用于触发 DOM 事件的工具,适用于大多数事件类型。你可以使用fireEvent
来模拟各种用户操作,例如点击、输入、聚焦等。
常用的 fireEvent
API
- click:模拟点击事件。
- change:模拟输入框的值变化事件。
- submit:模拟表单提交事件。
- focus:模拟聚焦事件。
- blur:模拟失焦事件。
- keydown、keyup、keypress:模拟键盘事件。
示例:
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
test('handles button click', () => {
render(<MyComponent />);
const button = screen.getByRole('button', { name: 'Click me' });
// 模拟点击事件
fireEvent.click(button);
// 断言点击后的结果
expect(screen.getByText('Button clicked')).toBeInTheDocument();
});
userEvent
userEvent
是@testing-library/user-event
库提供的,用于模拟用户操作,提供了更高级和更符合用户交互习惯的事件触发方法。
首先,确保安装了 @testing-library/user-event
包:
pnpm install --save-dev @testing-library/user-event
常用的 userEvent
API
- click:模拟点击事件。
- type:模拟用户输入事件。
- clear:清除输入框的内容。
- selectOptions:选择下拉菜单的选项。
- deselectOptions:取消选择下拉菜单的选项。
- upload:模拟文件上传事件。
- tab:模拟 tab 键切换焦点事件。
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './MyComponent';
test('handles input change', async () => {
render(<MyComponent />);
const input = screen.getByPlaceholderText('Enter text');
// 模拟输入事件
await userEvent.type(input, 'Hello, World!');
// 断言输入后的结果
expect(input).toHaveValue('Hello, World!');
});
选择 fireEvent
或 userEvent
fireEvent
: 更基础,更直接地触发事件,更适合简单的事件。userEvent
: 更高层次,更接近真实用户行为,适合模拟复杂的用户交互。
建议在大多数情况下使用 userEvent
,因为它能够更准确地模拟用户行为,从而使测试更加可靠和易读。但在一些简单的场景下,fireEvent
也足够使用。
断言结果
在使用 React Testing Library 进行测试时,断言(assertion)是验证组件行为和状态的关键步骤。React Testing Library 通常与 Jest 一起使用,Jest 提供了丰富的断言方法,而 React Testing Library 通过其扩展库
@testing-library/jest-dom
提供了一些额外的 DOM 相关的断言方法。
Jest
内置的断言方法
Jest
本身提供了一系列通用的断言方法,用于各种类型的验证。
常用的 Jest 断言方法
expect(value).toBe(expected)
: 验证两个值是否相等(严格相等)。expect(value).toEqual(expected)
: 验证两个对象或数组的结构是否相等。expect(value).toBeNull()
: 验证值是否为null
。expect(value).toBeUndefined()
: 验证值是否为undefined
。expect(value).toBeTruthy()
: 验证值是否为真(truthy)。expect(value).toBeFalsy()
: 验证值是否为假(falsy)。
@testing-library/jest-dom
提供的断言方法
@testing-library/jest-dom
扩展了 Jest 的断言方法,提供了一些专门用于 DOM 元素的断言工具,这些工具更直观,更适合测试 UI 组件。
安装 @testing-library/jest-dom
首先需要安装 @testing-library/jest-dom
:
npm install @testing-library/jest-dom
在测试文件中引入 @testing-library/jest-dom
:
import '@testing-library/jest-dom/extend-expect';
常用的 @testing-library/jest-dom
断言方法
-
toBeInTheDocument()
: 验证元素是否存在于文档中。expect(element).toBeInTheDocument();
-
toBeVisible()
: 验证元素是否可见。expect(element).toBeVisible();
-
toHaveTextContent(text)
: 断言元素具有特定的文本内容。expect(element).toHaveTextContent('Hello, World!');
-
toHaveAttribute(name, value)
: 验证元素是否具有指定属性及其值。expect(element).toHaveAttribute('type', 'submit');
-
toBeDisabled()
:断言元素被禁用。expect(element).toBeDisabled();
-
toBeChecked()
:断言复选框或单选按钮被选中。expect(checkbox).toBeChecked();
-
toHaveFocus()
:断言元素获得焦点。expect(input).toHaveFocus();
-
toHaveValue()
:断言表单元素具有特定的值。expect(input).toHaveValue('Initial value');
-
toHaveClass(className)
: 验证元素是否具有指定的 CSS 类。expect(element).toHaveClass('my-class');
-
toHaveStyle(css)
: 验证元素是否具有指定的内联样式。expect(element).toHaveStyle('display: none');
-
toHaveFormValues()
:断言表单元素具有特定的值。expect(form).toHaveFormValues({ username: 'johndoe', password: 'password123', });
-
toContainElement()
:断言一个元素包含另一个元素。expect(container).toContainElement(childElement);
-
toBeEmptyDOMElement()
:断言元素没有任何子节点。expect(element).toBeEmptyDOMElement();
以下是一些示例代码,展示了如何使用这些断言方法:
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect'; // 引入 jest-dom 扩展
import MyComponent from './MyComponent';
test('renders the component with correct text', () => {
render(<MyComponent />);
const linkElement = screen.getByText(/hello world/i);
expect(linkElement).toBeInTheDocument();
expect(linkElement).toBeVisible();
expect(linkElement).toHaveTextContent('Hello World');
});
test('input field has correct placeholder and value', () => {
render(<MyComponent />);
const inputElement = screen.getByPlaceholderText(/enter your name/i);
expect(inputElement).toBeInTheDocument();
expect(inputElement).toHaveAttribute('type', 'text');
expect(inputElement).toHaveValue('');
});
test('button has correct class and is disabled', () => {
render(<MyComponent />);
const buttonElement = screen.getByRole('button', { name: /submit/i });
expect(buttonElement).toBeInTheDocument();
expect(buttonElement).toHaveClass('submit-button');
expect(buttonElement).toBeDisabled();
});
通过结合 Jest 和 @testing-library/jest-dom
提供的断言方法,你可以编写清晰且有力的测试用例,验证组件的各种行为和状态。这些断言方法使得你的测试更加语义化和易读,从而提高了测试的可靠性和维护性。
常见的全局函数和钩子函数
-
beforeAll(fn, timeout):
- 在所有测试用例开始前执行,通常用于设置测试环境。
fn
: 函数,要执行的操作。timeout
: 可选参数,设置超时时间(毫秒)。
beforeAll(() => { // 执行一些初始化操作 });
-
afterAll(fn, timeout):
- 在所有测试用例结束后执行,通常用于清理测试环境。
fn
: 函数,要执行的清理操作。timeout
: 可选参数,设置超时时间(毫秒)。
afterAll(() => { // 执行一些清理操作 });
-
beforeEach(fn, timeout):
- 在每个测试用例开始前执行,通常用于重置测试状态。
fn
: 函数,要执行的操作。timeout
: 可选参数,设置超时时间(毫秒)。
beforeEach(() => { // 执行一些每个测试用例前的操作 });
-
afterEach(fn, timeout):
- 在每个测试用例结束后执行,通常用于清理测试状态。
fn
: 函数,要执行的清理操作。timeout
: 可选参数,设置超时时间(毫秒)。
afterEach(() => { // 执行一些每个测试用例后的操作 });
-
describe(name, fn):
- 定义一个测试套件(组),用来组织一组相关的测试用例。
name
: 字符串,套件(组)的名称。fn
: 函数,包含测试用例的回调函数。
describe('Math operations', () => { test('adds 1 + 2 to equal 3', () => { expect(1 + 2).toBe(3); }); });
-
test(name, fn, timeout):
- 定义一个测试用例。
name
: 字符串,测试用例的名称。fn
: 函数,包含测试用例的回调函数。timeout
: 可选参数,设置超时时间(毫秒)。
test('adds 1 + 2 to equal 3', () => { expect(1 + 2).toBe(3); });
模拟后台接口
在使用 React Testing Library 测试依赖于后台接口数据的组件时,我们通常会模拟这些接口调用。这可以通过以下几种方式实现:
- Mocking Fetch API:使用 Jest 来模拟全局
fetch
函数。 - Mocking Axios:如果使用 Axios 作为 HTTP 客户端,可以使用 Jest 来模拟 Axios。
- Mock Service Worker (MSW):一种更为强大和灵活的方式,模拟浏览器的网络请求。
下面我将分别介绍这几种方法。
Mocking Fetch API
假设我们有一个组件 UserComponent
,它在挂载时从 API 获取用户数据:
import React, { useEffect, useState } from 'react';
function UserComponent() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(response => response.json())
.then(data => setUser(data));
}, []);
if (!user) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
export default UserComponent;
测试示例:
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import UserComponent from './UserComponent';
beforeEach(() => {
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'John Doe', email: 'john.doe@example.com' }),
})
);
});
afterEach(() => {
global.fetch.mockClear();
});
test('fetches and displays user data', async () => {
render(<UserComponent />);
await waitFor(() => screen.getByText('John Doe'));
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
});
Mocking Axios
如果你使用 Axios 作为 HTTP 客户端,可以使用 Jest 来模拟 Axios 请求。
组件示例:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function UserComponent() {
const [user, setUser] = useState(null);
useEffect(() => {
axios.get('/api/user').then(response => {
setUser(response.data);
});
}, []);
if (!user) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
export default UserComponent;
测试示例:
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import axios from 'axios';
import UserComponent from './UserComponent';
jest.mock('axios');
test('fetches and displays user data', async () => {
axios.get.mockResolvedValue({ data: { name: 'John Doe', email: 'john.doe@example.com' } });
render(<UserComponent />);
await waitFor(() => screen.getByText('John Doe'));
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
});
Mock Service Worker (MSW)
Mock Service Worker (MSW) 是一个用于拦截和模拟浏览器网络请求的工具。它提供了更灵活和真实的模拟 HTTP 请求的方式。
安装 MSW:
npm install msw --save-dev
设置 MSW:
- 创建 mock server 配置文件(如
src/mocks/browser.js
):
import { setupWorker, rest } from 'msw';
export const worker = setupWorker(
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.json({ name: 'John Doe', email: 'john.doe@example.com' }));
})
);
- 在测试文件中启动 mock server:
import { render, screen, waitFor } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import UserComponent from './UserComponent';
// 设置服务端
const server = setupServer(
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.json({ name: 'John Doe', email: 'john.doe@example.com' }));
})
);
// 启动服务端
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('fetches and displays user data', async () => {
render(<UserComponent />);
await waitFor(() => screen.getByText('John Doe'));
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
});
总结
- Mocking Fetch API:适合直接使用
fetch
的情况,通过全局fetch
函数进行模拟。 - Mocking Axios:适合使用 Axios 作为 HTTP 客户端的情况,通过 Jest 模拟 Axios 请求。
- Mock Service Worker (MSW):更为灵活和强大的解决方案,适合复杂的网络请求模拟场景。
选择合适的方法来模拟后台接口数据,可以帮助你更好地测试依赖于 API 调用的组件。
其他
act
act
的作用是将你对组件状态或 DOM 的更新包裹起来,确保这些更新在测试中得到正确处理。这在测试异步操作或需要等待某些更新时特别有用。
异步示例
对于包含异步操作的组件,我们通常需要在 act
中处理这些异步操作。
假设我们有一个组件,点击按钮后异步获取数据并更新显示:
import React, { useState } from 'react';
function AsyncComponent() {
const [data, setData] = useState(null);
const fetchData = () => {
setTimeout(() => {
setData('Hello World');
}, 1000);
};
return (
<div>
<button onClick={fetchData}>Fetch Data</button>
{data ? <span>{data}</span> : <span>Loading...</span>}
</div>
);
}
我们可以这样测试它:
import { render, screen, fireEvent, act } from '@testing-library/react';
import AsyncComponent from './AsyncComponent';
test('fetches and displays data', async () => {
render(<AsyncComponent />);
const button = screen.getByText('Fetch Data');
act(() => {
fireEvent.click(button);
});
// 等待异步操作完成
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
});
expect(screen.getByText('Hello World')).toBeInTheDocument();
});
act
是确保在测试中所有更新都被正确处理的关键工具。- 在同步操作中,
act
将状态更新或事件处理包裹起来。 - 在异步操作中,
act
需要与async
/await
一起使用,确���异步操作完成并处理所有副作用。
使用 act
可以确保你的测试更加稳定和可靠,避免由于未处理的状态更新或副作用导致的测试失败。
renderHook
renderHook
用于测试自定义 Hook
。它使得我们能够单独测试 Hook 的逻辑,而不必将其嵌入到组件中。
用法:
import { renderHook } from '@testing-library/react-hooks';
const { result } = renderHook(() => useCustomHook());
示例:
假设我们有一个自定义 Hook useCounter
:
import { useState } from 'react';
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return { count, increment };
}
我们可以这样测试它:
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
renderHook
:用于渲染和测试自定义 Hook,简化 Hook 的独立测试。
waitFor
waitFor
是一个异步的工具函数,允许我们等待某个条件变为真。在测试异步代码或需要等待某些 DOM 更新时非常有用。
用法:
import { waitFor } from '@testing-library/react';
await waitFor(() => {
// 期待某个条件为真
expect(someElement).toBeInTheDocument();
});
示例:
假设我们有一个组件,在异步获取数据后更新 DOM:
import React, { useEffect, useState } from 'react';
function FetchComponent() {
const [data, setData] = useState(null);
useEffect(() => {
setTimeout(() => {
setData('Hello World');
}, 1000);
}, []);
if (!data) {
return <div>Loading...</div>;
}
return <div>{data}</div>;
}
我们可以这样测试它:
import { render, screen, waitFor } from '@testing-library/react';
import FetchComponent from './FetchComponent';
test('renders fetched data', async () => {
render(<FetchComponent />);
// 验证 'Loading...' 文本出现
expect(screen.getByText('Loading...')).toBeInTheDocument();
// 等待并验证 'Hello World' 文本出现
await waitFor(() => {
expect(screen.getByText('Hello World')).toBeInTheDocument();
});
});
waitFor
:用于等待异步操作完成,通常用于等待 DOM 更新或状态变化。
单元测试有什么好处呢
使用React Testing Library进行单元测试有以下好处:
- 增强代码的稳定性和可靠性:通过编写单元测试,可以及早捕获潜在的bug和错误,从而提高代码的质量和稳定性。单元测试可以帮助开发者在修改或重构代码时快速发现问题,并减少引入新问题的风险。
- 起到很好的说明文档作用:React Testing Library鼓励编写清晰、简洁和易于理解的测试代码。它专注于用户界面的行为和交互,而不是关注组件的内部实现细节。这样可以使测试代码更易读、易于维护,并且可以更好地与其他开发者共享和理解。
- 提升团队合作和沟通:React Testing Library提供了一种标准化的测试方法,使得团队成员之间的沟通更加容易。开发者可以更容易地理解测试代码,共享测试用例,并进行讨论和反馈。这有助于提高团队的合作效率和代码质量。
- 促进组件的正确交互:React Testing Library鼓励测试从用户的角度出发,通过模拟用户与组件的交互来检查组件的行为是否正确。这种方法可以更好地测试组件的交互逻辑,确保用户在实际使用时获得预期的体验。
- 支持重构和代码重用:在编写测试时,需要对组件的可测试性进行考虑。通过将测试与组件的内部实现解耦,可以更轻松地进行重构和代码重用。这有助于保持代码的灵活性和可维护性,并减少对测试的影响。
总的来说,使用React Testing Library进行单元测试可以提供可读性强的测试代码、促进组件的正确交互、增强代码的稳定性和可靠性、支持重构和代码重用,以及提升团队合作和沟通。这些好处有助于改善开发流程,提高代码质量,并提供更好的用户体验。
参考文献
转载自:https://juejin.cn/post/7381373703081738280