精通 React Testing Library 的组件测试。学习编写可维护、有效的测试的最佳实践,重点关注用户行为和可访问性。
React Testing Library:面向全球团队的组件测试最佳实践
在瞬息万变的 Web 开发世界中,确保 React 应用程序的可靠性和质量至关重要。对于在拥有多样化用户群和可访问性要求的项目上工作的全球团队来说,尤其如此。React Testing Library (RTL) 提供了一种强大且以用户为中心的组件测试方法。与专注于实现细节的传统测试方法不同,RTL 鼓励您像用户与之交互一样测试组件,从而使测试更加健壮和可维护。本综合指南将深入探讨在您的 React 项目中利用 RTL 的最佳实践,重点是构建适合全球受众的应用程序。
为何选择 React Testing Library?
在深入探讨最佳实践之前,了解 RTL 为何在众多测试库中脱颖而出至关重要。以下是其一些关键优势:
- 以用户为中心的方法: RTL 优先从用户视角测试组件。您会使用与用户相同的方法与组件交互(例如,点击按钮、在输入字段中键入),确保了更真实、更可靠的测试体验。
- 关注可访问性: RTL 通过鼓励您以考虑残障用户的方式进行测试,从而促进编写可访问的组件。这与 WCAG 等全球可访问性标准保持一致。
- 减少维护成本: 通过避免测试实现细节(例如,内部状态、特定函数调用),RTL 测试在您重构代码时不易损坏。这使得测试更易于维护且更具弹性。
- 改进代码设计: RTL 以用户为中心的方法通常会带来更好的组件设计,因为它迫使您思考用户将如何与您的组件进行交互。
- 社区与生态系统: RTL 拥有一个庞大而活跃的社区,提供充足的资源、支持和扩展。
设置您的测试环境
要开始使用 RTL,您需要设置您的测试环境。以下是使用 Create React App (CRA) 的基本设置,它已预先配置了 Jest 和 RTL:
npx create-react-app my-react-app
cd my-react-app
npm install --save-dev @testing-library/react @testing-library/jest-dom
说明:
- `npx create-react-app my-react-app`:使用 Create React App 创建一个新的 React 项目。
- `cd my-react-app`:进入新创建的项目目录。
- `npm install --save-dev @testing-library/react @testing-library/jest-dom`:将必要的 RTL 包作为开发依赖项安装。`@testing-library/react` 提供核心 RTL 功能,而 `@testing-library/jest-dom` 提供有用的 Jest 匹配器来处理 DOM。
如果您不使用 CRA,则需要单独安装 Jest 和 RTL,并配置 Jest 以使用 RTL。
使用 React Testing Library 进行组件测试的最佳实践
1. 编写模拟用户交互的测试
RTL 的核心原则是像用户一样测试组件。这意味着关注用户看到和做的事情,而不是内部实现细节。使用 RTL 提供的 `screen` 对象,根据元素的文本、角色或可访问性标签来查询它们。
示例:测试按钮点击
假设您有一个简单的按钮组件:
// Button.js
import React from 'react';
function Button({ onClick, children }) {
return ;
}
export default Button;
以下是使用 RTL 测试它的方法:
// Button.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button Component', () => {
it('calls the onClick handler when clicked', () => {
const handleClick = jest.fn();
render();
const buttonElement = screen.getByText('Click Me');
fireEvent.click(buttonElement);
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
说明:
- `render()`:使用一个模拟的 `onClick` 处理程序渲染 Button 组件。
- `screen.getByText('Click Me')`:在文档中查询包含文本“Click Me”的元素。这是用户识别按钮的方式。
- `fireEvent.click(buttonElement)`:模拟按钮元素的点击事件。
- `expect(handleClick).toHaveBeenCalledTimes(1)`:断言 `onClick` 处理程序被调用了一次。
为什么这比测试实现细节更好: 想象一下,您重构了 Button 组件以使用不同的事件处理程序或更改了内部状态。如果您测试的是特定的事件处理函数,您的测试就会失败。通过专注于用户交互(点击按钮),即使在重构之后,测试仍然有效。
2. 根据用户意图优先选择查询
RTL 提供了不同的查询方法来查找元素。请按以下顺序优先使用这些查询,因为它们最能反映用户如何感知您的组件并与之交互:
- getByRole: 这是最易于访问的查询,应是您的首选。它允许您根据元素的 ARIA 角色(例如,button、link、heading)查找元素。
- getByLabelText: 使用此方法查找与特定标签关联的元素,例如输入字段。
- getByPlaceholderText: 使用此方法根据占位符文本查找输入字段。
- getByText: 使用此方法根据文本内容查找元素。请具体说明,避免使用可能出现在多个地方的通用文本。
- getByDisplayValue: 使用此方法根据当前值查找输入字段。
示例:测试表单输入
// Input.js
import React from 'react';
function Input({ label, placeholder, value, onChange }) {
return (
);
}
export default Input;
以下是使用推荐的查询顺序测试它的方法:
// Input.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Input from './Input';
describe('Input Component', () => {
it('updates the value when the user types', () => {
const handleChange = jest.fn();
render();
const inputElement = screen.getByLabelText('Name');
fireEvent.change(inputElement, { target: { value: 'John Doe' } });
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(expect.objectContaining({ target: { value: 'John Doe' } }));
});
});
说明:
- `screen.getByLabelText('Name')`:使用 `getByLabelText` 查找与标签“Name”关联的输入字段。这是定位该输入字段最易于访问和用户友好的方式。
3. 避免测试实现细节
如前所述,避免测试内部状态、函数调用或特定的 CSS 类。这些是实现细节,容易发生变化,可能导致测试变得脆弱。专注于组件的可观察行为。
示例:避免直接测试状态
不要测试某个特定状态变量是否已更新,而应测试组件是否根据该状态呈现了正确的输出。例如,如果一个组件根据布尔状态变量显示一条消息,则应测试该消息是显示还是隐藏,而不是测试状态变量本身。
4. 在特定情况下使用 `data-testid`
虽然通常最好避免使用 `data-testid` 属性,但在某些特定情况下它们会很有用:
- 没有语义的元素: 如果您需要定位一个没有有意义的角色、标签或文本的元素,您可以使用 `data-testid`。
- 复杂的组件结构: 在复杂的组件结构中,`data-testid` 可以帮助您定位特定元素,而不依赖于脆弱的选择器。
- 可访问性测试: `data-testid` 可用于在使用 Cypress 或 Playwright 等工具进行可访问性测试时识别特定元素。
示例:使用 `data-testid`
// MyComponent.js
import React from 'react';
function MyComponent() {
return (
This is my component.
);
}
export default MyComponent;
// MyComponent.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('renders the component container', () => {
render( );
const containerElement = screen.getByTestId('my-component-container');
expect(containerElement).toBeInTheDocument();
});
});
重要提示: 请谨慎使用 `data-testid`,仅在其他查询方法不适用时使用。
5. 编写有意义的测试描述
清晰简洁的测试描述对于理解每个测试的目的和调试失败至关重要。使用能清楚解释测试正在验证什么内容的描述性名称。
示例:好的与坏的测试描述
坏的: `it('works')`
好的: `it('displays the correct greeting message')`
更好的: `it('displays the greeting message "Hello, World!" when the name prop is not provided')`
更好的示例清楚地说明了在特定条件下组件的预期行为。
6. 保持测试小而专注
每个测试都应专注于验证组件行为的单个方面。避免编写涵盖多种场景的大型复杂测试。小而专注的测试更容易理解、维护和调试。
7. 恰当使用测试替身(Mocks 和 Spies)
测试替身对于将您正在测试的组件与其依赖项隔离开来非常有用。使用 mocks 和 spies 来模拟外部服务、API 调用或其他组件。
示例:模拟 API 调用
// UserList.js
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
}
fetchUsers();
}, []);
return (
{users.map(user => (
- {user.name}
))}
);
}
export default UserList;
// UserList.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
]),
})
);
describe('UserList Component', () => {
it('fetches and displays a list of users', async () => {
render( );
// Wait for the data to load
await waitFor(() => screen.getByText('John Doe'));
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
说明:
- `global.fetch = jest.fn(...)`:模拟 `fetch` 函数以返回预定义的用户列表。这使您可以在不依赖真实 API 端点的情况下测试组件。
- `await waitFor(() => screen.getByText('John Doe'))`:等待“John Doe”文本出现在文档中。这是必需的,因为数据是异步获取的。
8. 测试边缘情况和错误处理
不要只测试“理想路径”。确保测试边缘情况、错误场景和边界条件。这将帮助您及早发现潜在问题,并确保您的组件能够优雅地处理意外情况。
示例:测试错误处理
假设一个组件从 API 获取数据,并在 API 调用失败时显示错误消息。您应该编写一个测试来验证当 API 调用失败时是否正确显示了错误消息。
9. 关注可访问性
可访问性对于创建包容性的 Web 应用程序至关重要。使用 RTL 测试组件的可访问性,并确保它们符合 WCAG 等可访问性标准。一些关键的可访问性考虑因素包括:
- 语义化 HTML: 使用语义化 HTML 元素(例如,`
- ARIA 属性: 使用 ARIA 属性提供有关元素角色、状态和属性的附加信息,特别是对于自定义组件。
- 键盘导航: 确保所有交互式元素都可以通过键盘导航访问。
- 颜色对比度: 使用足够的颜色对比度,以确保文本对于低视力用户是可读的。
- 屏幕阅读器兼容性: 使用屏幕阅读器测试您的组件,以确保它们为视力障碍用户提供有意义且易于理解的体验。
示例:使用 `getByRole` 测试可访问性
// MyAccessibleComponent.js
import React from 'react';
function MyAccessibleComponent() {
return (
);
}
export default MyAccessibleComponent;
// MyAccessibleComponent.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import MyAccessibleComponent from './MyAccessibleComponent';
describe('MyAccessibleComponent', () => {
it('renders an accessible button with the correct aria-label', () => {
render( );
const buttonElement = screen.getByRole('button', { name: 'Close' });
expect(buttonElement).toBeInTheDocument();
});
});
说明:
- `screen.getByRole('button', { name: 'Close' })`:使用 `getByRole` 查找具有可访问名称“Close”的按钮元素。这确保了该按钮为屏幕阅读器正确标记。
10. 将测试集成到您的开发工作流程中
测试应该是您开发工作流程中不可或缺的一部分,而不是事后才考虑的事情。将您的测试集成到 CI/CD 管道中,以便在提交或部署代码时自动运行测试。这将帮助您及早发现错误并防止回归。
11. 考虑本地化和国际化 (i18n)
对于全球性应用程序,在测试期间考虑本地化和国际化 (i18n) 至关重要。确保您的组件在不同语言和区域设置中都能正确呈现。
示例:测试本地化
如果您正在使用像 `react-intl` 或 `i18next` 这样的库进行本地化,您可以在测试中模拟本地化上下文,以验证您的组件是否显示了正确的翻译文本。
12. 使用自定义渲染函数实现可重用的设置
在处理大型项目时,您可能会发现自己在多个测试中重复相同的设置步骤。为避免重复,可以创建封装通用设置逻辑的自定义渲染函数。
示例:自定义渲染函数
// test-utils.js
import React from 'react';
import { render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import theme from './theme';
const AllTheProviders = ({ children }) => {
return (
{children}
);
}
const customRender = (ui, options) =>
render(ui, { wrapper: AllTheProviders, ...options })
// re-export everything
export * from '@testing-library/react'
// override render method
export { customRender as render }
// MyComponent.test.js
import React from 'react';
import { render, screen } from './test-utils'; // Import the custom render
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('renders correctly with the theme', () => {
render( );
// Your test logic here
});
});
此示例创建了一个自定义渲染函数,该函数使用 ThemeProvider 包装组件。这使您可以轻松测试依赖于主题的组件,而无需在每个测试中重复 ThemeProvider 的设置。
结论
React Testing Library 提供了一种强大且以用户为中心的组件测试方法。通过遵循这些最佳实践,您可以编写可维护、有效的测试,重点关注用户行为和可访问性。这将为全球受众带来更健壮、可靠和包容的 React 应用程序。请记住优先考虑用户交互,避免测试实现细节,关注可访问性,并将测试集成到您的开发工作流程中。通过拥抱这些原则,您可以构建满足全球用户需求的高质量 React 应用程序。
关键要点:
- 关注用户交互: 像用户与之交互一样测试组件。
- 优先考虑可访问性: 确保您的组件对残障用户是可访问的。
- 避免实现细节: 不要测试内部状态或函数调用。
- 编写清晰简洁的测试: 使您的测试易于理解和维护。
- 将测试集成到您的工作流程中: 自动化您的测试并定期运行它们。
- 考虑全球受众: 确保您的组件在不同语言和区域设置中都能良好工作。