Jest 和 React Testing Library (RTL) 是前端开发中用于测试 React 应用的首选工具。Jest 是一个功能丰富的JavaScript测试框架,而React Testing Library 是一种提倡以用户角度编写测试的库,它鼓励测试组件的行为而不是内部实现细节。
安装和配置
首先,确保你已经安装了react
, react-dom
, jest
, @testing-library/react
, 和 @testing-library/jest-dom
。在你的package.json
中添加以下依赖:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
# 或
yarn add --dev jest @testing-library/react @testing-library/jest-dom
在jest.config.js
中配置Jest,例如:
module.exports = {
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
testEnvironment: 'jsdom',
};
基本测试结构
创建一个测试文件,通常与你的组件文件同名,但带有.test.js
或.test.tsx
后缀。下面是一个简单的组件测试示例:
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent />);
expect(screen.getByText('Hello, world!')).toBeInTheDocument();
});
it('handles button click', () => {
render(<MyComponent />);
const button = screen.getByRole('button', { name: /click me/i });
fireEvent.click(button);
expect(screen.getByText(/clicked/i)).toBeInTheDocument();
});
});
测试组件行为
使用render
函数渲染组件,并使用screen
对象来查询DOM,确保组件按预期渲染。getByText
, getByRole
, getByPlaceholderText
等辅助函数可以帮助找到元素。
模拟(Mocking)
Jest 提供了强大的模拟功能,可以模拟组件的依赖,例如API调用。例如,模拟一个fetch
调用:
import fetch from 'jest-fetch-mock';
beforeAll(() => {
fetch.mockResponseOnce(JSON.stringify({ data: 'mocked response' }));
});
it('fetches data on mount', async () => {
render(<MyComponent />);
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
});
事件处理
使用fireEvent
函数触发组件上的事件,比如点击按钮或提交表单。
const button = screen.getByRole('button');
fireEvent.click(button);
清理和解构
在每个测试之后,确保清理掉任何副作用,如添加到DOM中的元素。afterEach钩子可以用于此目的:
afterEach(() => {
cleanup();
});
异步测试
使用waitFor
或async/await
处理异步操作,确保组件在测试中达到期望状态:
it('loads data after fetching', async () => {
render(<MyComponent />);
await waitFor(() => expect(screen.getByText('Data loaded')).toBeInTheDocument());
});
测试状态和副作用
使用jest.useFakeTimers()
和act
函数来测试状态变化和副作用,如定时器或副作用函数:
jest.useFakeTimers();
it('displays loading state', () => {
render(<MyComponent />);
act(() => jest.advanceTimersByTime(1000));
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
组件库的测试
对于复杂的组件库,可以创建一个setupTests.js
文件来设置全局的模拟和配置,例如:
import '@testing-library/jest-dom';
import fetchMock from 'jest-fetch-mock';
fetchMock.enableMocks(); // 如果使用fetch模拟
性能优化
使用jest-environment-jsdom-sixteen
或jest-environment-jsdom-thirteen
可以减少测试的内存消耗。
测试组件的交互性
React Testing Library 强调测试组件的行为,而不是它的实现细节。以下是一些测试组件交互性的最佳实践:
测试用户交互
使用fireEvent
模拟用户行为,例如点击、输入和选择:
const input = screen.getByLabelText('Search');
fireEvent.change(input, { target: { value: 'search term' } });
expect(input).toHaveValue('search term');
确保组件响应变化
测试组件如何响应状态或props
的变化:
const toggleButton = screen.getByRole('button', { name: 'Toggle' });
fireEvent.click(toggleButton);
expect(screen.getByTestId('visible-element')).toBeInTheDocument();
验证数据渲染
测试组件是否正确呈现从API获取的数据:
const data = { title: 'Example Title' };
fetchMock.mockResponseOnce(JSON.stringify(data));
render(<MyComponent />);
await waitFor(() => expect(screen.getByText('Example Title')).toBeInTheDocument());
错误和异常处理
测试组件在错误发生时的行为,例如验证错误消息的显示:
it('displays error message when fetching fails', async () => {
fetchMock.mockRejectOnce(new Error('Network error'));
render(<MyComponent />);
await waitFor(() => expect(screen.getByText('Error: Network error')).toBeInTheDocument());
});
清晰的测试描述
编写有意义的测试描述,让测试结果易于理解:
it('renders search results when query is submitted', async () => {
// ...
});
测试组件的边缘情况
确保覆盖组件的所有边缘情况,包括空值、异常数据和边界条件:
it('displays loading state when data is fetching', () => {
render(<MyComponent isLoading />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('displays empty state when no data is found', () => {
render(<MyComponent data={[]} />);
expect(screen.getByText('No results found.')).toBeInTheDocument();
});
代码覆盖率报告
使用jest-coverage
插件生成代码覆盖率报告,确保有足够的测试覆盖:
npx jest --coverage
持续集成
将测试集成到持续集成(CI)流程中,确保代码质量始终如一:
# .github/workflows/test.yml (GitHub Actions)
name: Test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '14.x'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
高级测试技巧
Mocking和Spying
Jest 提供了模拟(mocking)和监听(spying)功能,用于控制和检查函数行为:
import myFunction from './myFunction';
jest.spyOn(myModule, 'myFunction');
// 在测试中调用函数
myFunction();
// 检查函数是否被调用
expect(myFunction).toHaveBeenCalled();
// 检查函数调用的具体参数
expect(myFunction).toHaveBeenCalledWith(expectedArgs);
// 重置模拟
myFunction.mockReset();
// 重置并清除模拟的返回值和调用记录
myFunction.mockClear();
// 恢复原函数
myFunction.mockRestore();
测试异步逻辑
使用async/await
和await waitFor
处理异步操作:
it('fetches data and updates state', async () => {
// 模拟API返回
fetchMock.mockResolvedValueOnce({ json: () => Promise.resolve({ data: 'mocked data' }) });
render(<MyComponent />);
// 等待数据加载完成
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
// 验证状态更新
expect(screen.getByText('mocked data')).toBeInTheDocument();
});
测试生命周期方法
使用act
包裹组件的生命周期方法,确保它们在测试环境中正确执行:
javascript
import { act } from 'react-dom/test-utils';
it('calls componentDidMount', () => {
const mockFn = jest.fn();
const MyComponent = () => {
useEffect(mockFn);
return <div>Component</div>;
};
act(() => {
render(<MyComponent />);
});
expect(mockFn).toHaveBeenCalled();
});
使用createRef和forwardRef
测试使用createRef
或forwardRef
的组件时,可以创建一个ref
并传递给组件:
it('sets focus on the input element', () => {
const inputRef = React.createRef();
render(<MyComponent inputRef={inputRef} />);
act(() => {
inputRef.current.focus();
});
expect(document.activeElement).toBe(inputRef.current);
});
测试事件处理器
使用fireEvent
模拟事件,但要确保在act中进行:
it('calls onChange handler', () => {
const onChangeHandler = jest.fn();
render(<MyComponent onChange={onChangeHandler} />);
const input = screen.getByRole('textbox');
act(() => {
fireEvent.change(input, { target: { value: 'new value' } });
});
expect(onChangeHandler).toHaveBeenCalledWith('new value');
});
性能优化
快速测试
减少渲染深度:只渲染必要的组件层级,避免渲染整个组件树。
使用jest.spyOn
代替jest.fn
:对于性能敏感的函数,使用jest.spyOn
代替jest.fn
,因为它更快。
选择性运行测试
使用--findRelatedTests选项只运行与更改相关的测试,以加快测试速度:
npx jest --findRelatedTests
使用快照测试
对于不经常更改的组件,使用快照测试可以节省时间:
it('renders snapshot correctly', () => {
const { container } = render(<MyComponent />);
expect(container.firstChild).toMatchSnapshot();
});
18.4 代码覆盖率阈值
设置代码覆盖率阈值,确保测试覆盖了足够的代码:
javascript
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
statements: 80,
branches: 80,
functions: 80,
lines: 80,
},
},
};