当前位置: 首页 > news >正文

React Testing完全指南:Jest、React Testing Library实战

在现代前端开发中,测试是确保代码质量和应用稳定性的关键环节。本文将深入探讨如何使用Jest和React Testing Library构建完整的React应用测试体系。

目录

  1. 测试基础概念
  2. Jest测试框架
  3. React Testing Library介绍
  4. 环境搭建
  5. 基础组件测试
  6. 用户交互测试
  7. 异步操作测试
  8. Mock和Spy
  9. 自定义Hook测试
  10. 集成测试
  11. 测试最佳实践

测试基础概念

测试类型

单元测试(Unit Testing)

  • 测试单个组件或函数的功能
  • 快速执行,易于调试
  • 应该占测试套件的大部分

集成测试(Integration Testing)

  • 测试多个组件协同工作
  • 验证组件间的交互
  • 更接近真实用户场景

端到端测试(E2E Testing)

  • 模拟真实用户操作
  • 测试完整的用户流程
  • 执行较慢,但提供最高信心

测试金字塔

    /\/E2E\     - 少量,覆盖关键用户流程/______\/Integration\ - 适量,测试组件交互
/______________\Unit Tests   - 大量,快速反馈

Jest测试框架

Jest是Facebook开发的JavaScript测试框架,提供了完整的测试解决方案。

核心特性

  • 零配置:开箱即用
  • 快照测试:自动生成和比较组件渲染结果
  • 内置断言:丰富的匹配器
  • 代码覆盖率:无需额外配置
  • 并行执行:提高测试速度

基本语法

// 基础测试结构
describe('Calculator', () => {test('should add two numbers', () => {expect(add(2, 3)).toBe(5);});test('should handle negative numbers', () => {expect(add(-1, 1)).toBe(0);});
});

常用匹配器

// 基本匹配器
expect(value).toBe(4); // 严格相等
expect(value).toEqual({name: 'John'}); // 深度相等
expect(value).toBeNull(); // null值
expect(value).toBeUndefined(); // undefined值
expect(value).toBeTruthy(); // 真值
expect(value).toBeFalsy(); // 假值// 数字匹配器
expect(value).toBeGreaterThan(3);
expect(value).toBeCloseTo(0.3);// 字符串匹配器
expect('team').toMatch(/I/);
expect('Christoph').toMatch('stop');// 数组匹配器
expect(['Alice', 'Bob', 'Eve']).toContain('Alice');// 异常匹配器
expect(() => {throw new Error('Wrong!');
}).toThrow('Wrong!');

React Testing Library介绍

React Testing Library基于"测试应该尽可能接近用户使用软件的方式"的理念设计。

核心原则

  1. 用户为中心:通过用户能看到和交互的方式测试
  2. 实现细节无关:不测试组件内部状态或方法
  3. 可访问性优先:鼓励编写可访问的组件

查询优先级

// 推荐优先级(从高到低)// 1. 对所有人可访问的查询
getByRole('button', {name: /submit/i})
getByLabelText(/username/i)
getByPlaceholderText(/enter username/i)
getByText(/hello world/i)// 2. 语义化查询
getByAltText(/profile picture/i)
getByTitle(/close/i)// 3. 测试ID(最后选择)
getByTestId('submit-button')

环境搭建

安装依赖

# 如果使用Create React App,已内置所需依赖
npx create-react-app my-app# 手动安装
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event

配置文件

// src/setupTests.js
import '@testing-library/jest-dom';// jest.config.js
module.exports = {testEnvironment: 'jsdom',setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],moduleNameMapping: {'\\.(css|less|scss|sass)$': 'identity-obj-proxy',},collectCoverageFrom: ['src/**/*.{js,jsx}','!src/index.js','!src/reportWebVitals.js',],
};

基础组件测试

简单组件测试

// components/Button.js
import React from 'react';const Button = ({ children, onClick, disabled = false, variant = 'primary' }) => {return (<buttononClick={onClick}disabled={disabled}className={`btn btn-${variant}`}data-testid="custom-button">{children}</button>);
};export default Button;
// components/__tests__/Button.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from '../Button';describe('Button Component', () => {test('renders button with text', () => {render(<Button>Click me</Button>);expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();});test('calls onClick when clicked', async () => {const user = userEvent.setup();const handleClick = jest.fn();render(<Button onClick={handleClick}>Click me</Button>);await user.click(screen.getByRole('button'));expect(handleClick).toHaveBeenCalledTimes(1);});test('is disabled when disabled prop is true', () => {render(<Button disabled>Click me</Button>);expect(screen.getByRole('button')).toBeDisabled();});test('applies correct CSS class for variant', () => {render(<Button variant="secondary">Click me</Button>);expect(screen.getByRole('button')).toHaveClass('btn-secondary');});
});

表单组件测试

// components/LoginForm.js
import React, { useState } from 'react';const LoginForm = ({ onSubmit }) => {const [username, setUsername] = useState('');const [password, setPassword] = useState('');const [error, setError] = useState('');const handleSubmit = (e) => {e.preventDefault();setError('');if (!username || !password) {setError('请填写所有字段');return;}onSubmit({ username, password });};return (<form onSubmit={handleSubmit}><div><label htmlFor="username">用户名:</label><inputid="username"type="text"value={username}onChange={(e) => setUsername(e.target.value)}/></div><div><label htmlFor="password">密码:</label><inputid="password"type="password"value={password}onChange={(e) => setPassword(e.target.value)}/></div>{error && <div role="alert">{error}</div>}<button type="submit">登录</button></form>);
};export default LoginForm;
// components/__tests__/LoginForm.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from '../LoginForm';describe('LoginForm', () => {test('renders form fields', () => {render(<LoginForm onSubmit={jest.fn()} />);expect(screen.getByLabelText(/用户名/i)).toBeInTheDocument();expect(screen.getByLabelText(/密码/i)).toBeInTheDocument();expect(screen.getByRole('button', { name: /登录/i })).toBeInTheDocument();});test('shows error when submitting empty form', async () => {const user = userEvent.setup();render(<LoginForm onSubmit={jest.fn()} />);await user.click(screen.getByRole('button', { name: /登录/i }));expect(screen.getByRole('alert')).toHaveTextContent('请填写所有字段');});test('calls onSubmit with form data when valid', async () => {const user = userEvent.setup();const mockSubmit = jest.fn();render(<LoginForm onSubmit={mockSubmit} />);await user.type(screen.getByLabelText(/用户名/i), 'testuser');await user.type(screen.getByLabelText(/密码/i), 'password123');await user.click(screen.getByRole('button', { name: /登录/i }));expect(mockSubmit).toHaveBeenCalledWith({username: 'testuser',password: 'password123'});});
});

用户交互测试

模拟用户事件

import userEvent from '@testing-library/user-event';// 点击事件
await user.click(element);// 输入文本
await user.type(input, 'hello world');// 选择选项
await user.selectOptions(select, 'option1');// 键盘事件
await user.keyboard('{Enter}');
await user.keyboard('{Escape}');// 复合操作
await user.clear(input);
await user.tab();

复杂交互示例

// components/TodoList.js
import React, { useState } from 'react';const TodoList = () => {const [todos, setTodos] = useState([]);const [inputValue, setInputValue] = useState('');const addTodo = () => {if (inputValue.trim()) {setTodos([...todos, { id: Date.now(), text: inputValue, completed: false }]);setInputValue('');}};const toggleTodo = (id) => {setTodos(todos.map(todo =>todo.id === id ? { ...todo, completed: !todo.completed } : todo));};const deleteTodo = (id) => {setTodos(todos.filter(todo => todo.id !== id));};return (<div><div><inputvalue={inputValue}onChange={(e) => setInputValue(e.target.value)}placeholder="添加新任务"onKeyPress={(e) => e.key === 'Enter' && addTodo()}/><button onClick={addTodo}>添加</button></div><ul>{todos.map(todo => (<li key={todo.id}><inputtype="checkbox"checked={todo.completed}onChange={() => toggleTodo(todo.id)}aria-label={`标记 "${todo.text}" 为${todo.completed ? '未完成' : '已完成'}`}/><span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>{todo.text}</span><button onClick={() => deleteTodo(todo.id)}>删除</button></li>))}</ul></div>);
};export default TodoList;
// components/__tests__/TodoList.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TodoList from '../TodoList';describe('TodoList', () => {test('adds new todo when button clicked', async () => {const user = userEvent.setup();render(<TodoList />);const input = screen.getByPlaceholderText(/添加新任务/i);const addButton = screen.getByRole('button', { name: /添加/i });await user.type(input, '学习React测试');await user.click(addButton);expect(screen.getByText('学习React测试')).toBeInTheDocument();expect(input).toHaveValue('');});test('adds todo when Enter key pressed', async () => {const user = userEvent.setup();render(<TodoList />);const input = screen.getByPlaceholderText(/添加新任务/i);await user.type(input, '学习Jest');await user.keyboard('{Enter}');expect(screen.getByText('学习Jest')).toBeInTheDocument();});test('toggles todo completion', async () => {const user = userEvent.setup();render(<TodoList />);// 添加任务await user.type(screen.getByPlaceholderText(/添加新任务/i), '测试任务');await user.click(screen.getByRole('button', { name: /添加/i }));// 切换完成状态const checkbox = screen.getByRole('checkbox');await user.click(checkbox);expect(checkbox).toBeChecked();expect(screen.getByText('测试任务')).toHaveStyle('text-decoration: line-through');});test('deletes todo', async () => {const user = userEvent.setup();render(<TodoList />);// 添加任务await user.type(screen.getByPlaceholderText(/添加新任务/i), '要删除的任务');await user.click(screen.getByRole('button', { name: /添加/i }));// 删除任务await user.click(screen.getByRole('button', { name: /删除/i }));expect(screen.queryByText('要删除的任务')).not.toBeInTheDocument();});
});

异步操作测试

API调用测试

// services/api.js
export const fetchUser = async (id) => {const response = await fetch(`/api/users/${id}`);if (!response.ok) {throw new Error('用户不存在');}return response.json();
};
// components/UserProfile.js
import React, { useState, useEffect } from 'react';
import { fetchUser } from '../services/api';const UserProfile = ({ userId }) => {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {const loadUser = async () => {try {setLoading(true);setError(null);const userData = await fetchUser(userId);setUser(userData);} catch (err) {setError(err.message);} finally {setLoading(false);}};if (userId) {loadUser();}}, [userId]);if (loading) return <div>加载中...</div>;if (error) return <div role="alert">错误: {error}</div>;if (!user) return <div>用户不存在</div>;return (<div><h2>{user.name}</h2><p>邮箱: {user.email}</p><p>电话: {user.phone}</p></div>);
};export default UserProfile;
// components/__tests__/UserProfile.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from '../UserProfile';
import { fetchUser } from '../../services/api';// Mock API调用
jest.mock('../../services/api');
const mockFetchUser = fetchUser as jest.MockedFunction<typeof fetchUser>;describe('UserProfile', () => {beforeEach(() => {mockFetchUser.mockClear();});test('shows loading state initially', () => {mockFetchUser.mockImplementation(() => new Promise(() => {})); // 永不resolverender(<UserProfile userId="1" />);expect(screen.getByText(/加载中/i)).toBeInTheDocument();});test('displays user data when loaded successfully', async () => {const mockUser = {id: '1',name: '张三',email: 'zhangsan@example.com',phone: '123-456-7890'};mockFetchUser.mockResolvedValue(mockUser);render(<UserProfile userId="1" />);await waitFor(() => {expect(screen.getByText('张三')).toBeInTheDocument();});expect(screen.getByText('邮箱: zhangsan@example.com')).toBeInTheDocument();expect(screen.getByText('电话: 123-456-7890')).toBeInTheDocument();});test('displays error when API call fails', async () => {mockFetchUser.mockRejectedValue(new Error('用户不存在'));render(<UserProfile userId="999" />);await waitFor(() => {expect(screen.getByRole('alert')).toHaveTextContent('错误: 用户不存在');});});test('refetches user when userId changes', async () => {const { rerender } = render(<UserProfile userId="1" />);await waitFor(() => {expect(mockFetchUser).toHaveBeenCalledWith('1');});rerender(<UserProfile userId="2" />);await waitFor(() => {expect(mockFetchUser).toHaveBeenCalledWith('2');});expect(mockFetchUser).toHaveBeenCalledTimes(2);});
});

延时操作测试

// components/SearchInput.js
import React, { useState, useEffect } from 'react';const SearchInput = ({ onSearch, delay = 300 }) => {const [query, setQuery] = useState('');useEffect(() => {const timer = setTimeout(() => {if (query) {onSearch(query);}}, delay);return () => clearTimeout(timer);}, [query, delay, onSearch]);return (<inputtype="text"value={query}onChange={(e) => setQuery(e.target.value)}placeholder="搜索..."/>);
};export default SearchInput;
// components/__tests__/SearchInput.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchInput from '../SearchInput';// 启用fake timers
jest.useFakeTimers();describe('SearchInput', () => {afterEach(() => {jest.clearAllTimers();});test('debounces search calls', async () => {const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });const mockSearch = jest.fn();render(<SearchInput onSearch={mockSearch} delay={300} />);const input = screen.getByPlaceholderText(/搜索/i);// 快速输入多个字符await user.type(input, 'react');// 此时还没有调用搜索expect(mockSearch).not.toHaveBeenCalled();// 快进时间jest.advanceTimersByTime(300);// 现在应该调用搜索await waitFor(() => {expect(mockSearch).toHaveBeenCalledWith('react');});expect(mockSearch).toHaveBeenCalledTimes(1);});test('cancels previous timer when typing quickly', async () => {const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });const mockSearch = jest.fn();render(<SearchInput onSearch={mockSearch} delay={300} />);const input = screen.getByPlaceholderText(/搜索/i);// 输入 'r'await user.type(input, 'r');jest.advanceTimersByTime(100);// 继续输入 'e'await user.type(input, 'e');jest.advanceTimersByTime(100);// 继续输入 'act'await user.type(input, 'act');// 快进到延时结束jest.advanceTimersByTime(300);// 应该只调用一次,使用最终的查询await waitFor(() => {expect(mockSearch).toHaveBeenCalledWith('react');});expect(mockSearch).toHaveBeenCalledTimes(1);});
});

Mock和Spy

模拟函数

// 创建mock函数
const mockFn = jest.fn();// 设置返回值
mockFn.mockReturnValue('mocked value');
mockFn.mockReturnValueOnce('first call');
mockFn.mockReturnValueOnce('second call');// 设置异步返回值
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('async error'));// 设置实现
mockFn.mockImplementation((x) => x * 2);
mockFn.mockImplementationOnce((x) => x * 3);// 验证调用
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('last arg');

模拟模块

// 完全模拟模块
jest.mock('../utils/helper');// 部分模拟模块
jest.mock('../utils/helper', () => ({...jest.requireActual('../utils/helper'),formatDate: jest.fn(() => '2023-01-01'),
}));// 自动模拟
jest.mock('../services/api');// 手动模拟
jest.mock('../services/api', () => ({fetchData: jest.fn(),postData: jest.fn(),
}));

Spy函数

// 监听对象方法
const obj = {method: () => 'original'
};const spy = jest.spyOn(obj, 'method');
spy.mockReturnValue('mocked');// 监听console.log
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();// 恢复原始实现
spy.mockRestore();

自定义Hook测试

renderHook使用

// hooks/useCounter.js
import { useState } from 'react';const useCounter = (initialValue = 0) => {const [count, setCount] = useState(initialValue);const increment = () => setCount(c => c + 1);const decrement = () => setCount(c => c - 1);const reset = () => setCount(initialValue);return { count, increment, decrement, reset };
};export default useCounter;
// hooks/__tests__/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from '../useCounter';describe('useCounter', () => {test('initializes with default value', () => {const { result } = renderHook(() => useCounter());expect(result.current.count).toBe(0);});test('initializes with custom value', () => {const { result } = renderHook(() => useCounter(10));expect(result.current.count).toBe(10);});test('increments count', () => {const { result } = renderHook(() => useCounter());act(() => {result.current.increment();});expect(result.current.count).toBe(1);});test('decrements count', () => {const { result } = renderHook(() => useCounter(5));act(() => {result.current.decrement();});expect(result.current.count).toBe(4);});test('resets to initial value', () => {const { result } = renderHook(() => useCounter(10));act(() => {result.current.increment();result.current.increment();});expect(result.current.count).toBe(12);act(() => {result.current.reset();});expect(result.current.count).toBe(10);});
});

异步Hook测试

// hooks/useFetch.js
import { useState, useEffect } from 'react';const useFetch = (url) => {const [data, setData] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {const fetchData = async () => {try {setLoading(true);setError(null);const response = await fetch(url);if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}const result = await response.json();setData(result);} catch (err) {setError(err.message);} finally {setLoading(false);}};if (url) {fetchData();}}, [url]);return { data, loading, error };
};export default useFetch;
// hooks/__tests__/useFetch.test.js
import { renderHook, waitFor } from '@testing-library/react';
import useFetch from '../useFetch';// Mock fetch
global.fetch = jest.fn();
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;describe('useFetch', () => {beforeEach(() => {mockFetch.mockClear();});test('returns initial state', () => {const { result } = renderHook(() => useFetch(''));expect(result.current.data).toBeNull();expect(result.current.loading).toBe(true);expect(result.current.error).toBeNull();});test('fetches data successfully', async () => {const mockData = { id: 1, name: 'Test' };mockFetch.mockResolvedValue({ok: true,json: async () => mockData,} as Response);const { result } = renderHook(() => useFetch('/api/test'));await waitFor(() => {expect(result.current.loading).toBe(false);});expect(result.current.data).toEqual(mockData);expect(result.current.error).toBeNull();});test('handles fetch error', async () => {mockFetch.mockResolvedValue({ok: false,status: 404,} as Response);const { result } = renderHook(() => useFetch('/api/not-found'));await waitFor(() => {expect(result.current.loading).toBe(false);});expect(result.current.data).toBeNull();expect(result.current.error).toBe('HTTP error! status: 404');});test('refetches when URL changes', async () => {const { result, rerender } = renderHook(({ url }) => useFetch(url),{ initialProps: { url: '/api/test1' } });await waitFor(() => {expect(result.current.loading).toBe(false);});rerender({ url: '/api/test2' });expect(result.current.loading).toBe(true);expect(mockFetch).toHaveBeenCalledTimes(2);});
});

集成测试

多组件协作测试

// components/ShoppingCart.js
import React, { useState } from 'react';
import ProductList from './ProductList';
import Cart from './Cart';const ShoppingCart = () => {const [cartItems, setCartItems] = useState([]);const addToCart = (product) => {setCartItems(prev => {const existing = prev.find(item => item.id === product.id);if (existing) {return prev.map(item =>item.id === product.id? { ...item, quantity: item.quantity + 1 }: item);}return [...prev, { ...product, quantity: 1 }];});};const removeFromCart = (productId) => {setCartItems(prev => prev.filter(item => item.id !== productId));};const updateQuantity = (productId, quantity) => {if (quantity <= 0) {removeFromCart(productId);return;}setCartItems(prev =>prev.map(item =>item.id === productId ? { ...item, quantity } : item));};return (<div><h1>购物商城</h1><div style={{ display: 'flex' }}><ProductList onAddToCart={addToCart} /><Cartitems={cartItems}onRemove={removeFromCart}onUpdateQuantity={updateQuantity}/></div></div>);
};export default ShoppingCart;
// components/__tests__/ShoppingCart.integration.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ShoppingCart from '../ShoppingCart';describe('ShoppingCart Integration', () => {test('complete shopping flow', async () => {const user = userEvent.setup();render(<ShoppingCart />);// 验证初始状态expect(screen.getByText('购物商城')).toBeInTheDocument();expect(screen.getByText('购物车空空如也')).toBeInTheDocument();// 添加第一个商品const firstAddButton = screen.getAllByText('添加到购物车')[0];await user.click(firstAddButton);// 验证购物车更新expect(screen.queryByText('购物车空空如也')).not.toBeInTheDocument();expect(screen.getByText(/商品1/)).toBeInTheDocument();expect(screen.getByText(/数量: 1/)).toBeInTheDocument();// 再次添加同一商品await user.click(firstAddButton);expect(screen.getByText(/数量: 2/)).toBeInTheDocument();// 添加第二个商品const secondAddButton = screen.getAllByText('添加到购物车')[1];await user.click(secondAddButton);// 验证两个商品都在购物车中expect(screen.getByText(/商品1/)).toBeInTheDocument();expect(screen.getByText(/商品2/)).toBeInTheDocument();// 更新数量const quantityInput = screen.getAllByRole('spinbutton')[0];await user.clear(quantityInput);await user.type(quantityInput, '5');expect(screen.getByDisplayValue('5')).toBeInTheDocument();// 删除商品const removeButton = screen.getAllByText('删除')[0];await user.click(removeButton);expect(screen.queryByText(/商品1/)).not.toBeInTheDocument();expect(screen.getByText(/商品2/)).toBeInTheDocument();// 计算总价expect(screen.getByText(/总计:/)).toBeInTheDocument();});test('handles edge cases', async () => {const user = userEvent.setup();render(<ShoppingCart />);// 添加商品const addButton = screen.getAllByText('添加到购物车')[0];await user.click(addButton);// 将数量设为0(应该删除商品)const quantityInput = screen.getByRole('spinbutton');await user.clear(quantityInput);await user.type(quantityInput, '0');expect(screen.getByText('购物车空空如也')).toBeInTheDocument();});
});

测试最佳实践

1. 测试结构组织

// 推荐的测试文件结构
describe('ComponentName', () => {// 设置和清理beforeEach(() => {// 通用设置});afterEach(() => {// 清理工作});// 按功能分组describe('rendering', () => {test('renders correctly with default props', () => {});test('renders correctly with custom props', () => {});});describe('user interactions', () => {test('handles click events', () => {});test('handles form submission', () => {});});describe('edge cases', () => {test('handles empty data', () => {});test('handles error states', () => {});});
});

2. 测试命名约定

// 好的测试名称
test('shows error message when form is submitted with empty email', () => {});
test('updates cart total when item quantity changes', () => {});
test('disables submit button when request is pending', () => {});// 避免的测试名称
test('test email validation', () => {});
test('cart functionality', () => {});
test('button state', () => {});

3. 断言最佳实践

// 精确断言
expect(screen.getByRole('button')).toBeEnabled();
expect(screen.getByText('Success!')).toBeInTheDocument();// 避免过于宽泛的断言
expect(container.firstChild).toBeTruthy(); // 不够具体// 使用语义化查询
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText(/email address/i);// 避免实现细节
expect(component.state.isLoading).toBe(false); // 测试实现细节

4. 辅助函数

// test-utils.js
import React from 'react';
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from '../store';// 自定义渲染函数
export const renderWithProviders = (ui, options = {}) => {const Wrapper = ({ children }) => (<Provider store={store}><BrowserRouter>{children}</BrowserRouter></Provider>);return render(ui, { wrapper: Wrapper, ...options });
};// 通用的等待函数
export const waitForLoadingToFinish = () => waitFor(() => expect(screen.queryByText(/loading/i)).not.toBeInTheDocument());// 表单填写辅助函数
export const fillForm = async (user, formData) => {for (const [field, value] of Object.entries(formData)) {const input = screen.getByLabelText(new RegExp(field, 'i'));await user.clear(input);await user.type(input, value);}
};

5. 代码覆盖率

# 运行测试并生成覆盖率报告
npm test -- --coverage# 设置覆盖率阈值
# package.json
{"jest": {"collectCoverageFrom": ["src/**/*.{js,jsx}","!src/index.js","!src/reportWebVitals.js"],"coverageThreshold": {"global": {"branches": 80,"functions": 80,"lines": 80,"statements": 80}}}
}

6. 性能测试

test('renders large list efficiently', () => {const largeData = Array.from({ length: 1000 }, (_, i) => ({id: i,name: `Item ${i}`}));const startTime = performance.now();render(<LargeList items={largeData} />);const endTime = performance.now();expect(endTime - startTime).toBeLessThan(100); // 100ms 以内
});

7. 快照测试

test('matches snapshot', () => {const { container } = render(<Button variant="primary">Click me</Button>);expect(container.firstChild).toMatchSnapshot();
});// 内联快照
test('renders correctly', () => {const { container } = render(<Button>Test</Button>);expect(container.firstChild).toMatchInlineSnapshot(`<buttonclass="btn btn-primary"data-testid="custom-button">Test</button>`);
});

总结

React测试是确保应用质量的重要环节。通过Jest和React Testing Library的组合,我们可以:

  • 编写用户导向的测试:测试用户实际看到和交互的内容
  • 保持测试稳定性:避免测试实现细节,专注于行为
  • 提高开发效率:快速反馈,及早发现问题
  • 增强重构信心:完善的测试覆盖让代码重构更安全

http://www.dtcms.com/a/398303.html

相关文章:

  • python+springboot+django/flask的医院食堂订餐系统 菜单发布 在线订餐 餐品管理与订单统计系统
  • 半导体制造常见检测之拉曼光谱
  • Python 第七节 循环语句for和while使用详解及注意事项
  • 怎么把svg做网站背景谷歌关键词挖掘工具
  • Vue3中的computed属性
  • 7. 临时变量的常量性
  • SNK施努卡有色冶炼自动化解决方案
  • SpringCloud项目阶段七:延迟任务技术选项对比以及接入redis实现延迟队列添加/取消/消费等任务
  • 建站特别慢wordpress网站项目总体设计模板
  • 驱动开发,为什么需要映射?
  • 网站栏目模版确定网站推广目标
  • AI产品经理项目实战:BERT语义分析识别重复信息
  • 亚远景-ISO 42001:为汽车AI安全设定新标杆
  • 电路方案分析(二十四)汽车高压互锁参考设计
  • 深圳网站快速备案手机app播放器
  • CSS精灵技术
  • 数据库导论#1
  • Web应用接入支付功能的准备工作和开发规范
  • 专业做logo的网站wordpress安装模板
  • 8 shiro的web整合
  • iOS 26 系统电耗分析实战指南 如何检测电池掉电、液体玻璃导致的能耗变化
  • 自动化平台自动化能力统一的建设
  • 做网站学的是代码吗网站备案流程教程
  • 【Unity 入门教程】二、核心概念
  • 【春秋云镜】CVE-2022-30887(文件上传/rce)
  • [iOS] YYModel 初步学习
  • 视频录屏软件 视频录屏软件 Bandicam (班迪录屏) 8.2.2.2531
  • 今天继续学习nginx服务部署与配置
  • flutter 编译报错java.util.zip.ZipException: zip END header not found
  • 网站建设精英京东商城网站域名