关于 react-testing
react-testing 是一项为 React 应用编写测试提供指导的技能,面向开发者和测试人员。内容涵盖 Vitest、React Testing Library 和 Jest 的组件、Hook、集成、模拟及无障碍测试,并强调验证用户行为而非实现细节。
主要功能
- 01测试 React 组件、表单和自定义 Hook
- 02编写带 Context 或 Router 的集成测试
- 03模拟 API 和组件依赖
- 04检查无障碍问题
- 05遵循以用户行为为核心的测试实践
适用场景
- 01为组件和表单编写单元测试
- 02验证 Hook 及异步数据行为
- 03测试路由和 Context 集成
- 04在 CI/CD 中运行回归测试
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ majesticlabs-dev/ majestic-marketplace --skill react-testing安装后在 Claude Code 中输入/react-testing确认已加载。
详细介绍
React 测试
概览
本技能提供使用 Vitest、React Testing Library 和 Jest 测试 React 应用的全面指导。在编写单元测试、集成测试和确保代码质量时,请采用这些模式。
核心理念
- 测试行为,而非实现:关注用户能看到和执行的操作
- 避免测试内部状态:测试公开 API 和用户交互
- 编写让人有信心的测试:捕获真实 Bug,而非假阳性
- 保持测试简单:测试应比代码更易理解
测试工具
Vitest 配置
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'src/test/']
}
}
});
测试设置
// src/test/setup.ts
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);
afterEach(() => {
cleanup();
});
组件测试
基本组件测试
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import { Button } from './Button';
test('renders button with label', () => {
render(<Button label="Click me" onClick={vi.fn()} />);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
test('calls onClick when clicked', async () => {
const handleClick = vi.fn();
render(<Button label="Click me" onClick={handleClick} />);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('is disabled when prop is set', () => {
render(<Button label="Click me" onClick={vi.fn()} disabled />);
expect(screen.getByRole('button')).toBeDisabled();
});
表单测试
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
test('submits form with email and password', async () => {
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await userEvent.type(screen.getByLabelText('Email'), '[email protected]');
await userEvent.type(screen.getByLabelText('Password'), 'password123');
await userEvent.click(screen.getByRole('button', { name: 'Login' }));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123'
});
});
});
test('shows validation error for invalid email', async () => {
render(<LoginForm onSubmit={vi.fn()} />);
await userEvent.type(screen.getByLabelText('Email'), 'invalid-email');
await userEvent.click(screen.getByRole('button', { name: 'Login' }));
expect(await screen.findByText('Invalid email address')).toBeInTheDocument();
});
Hook 测试
自定义 Hook 测试
import { renderHook, act } from '@testing-library/react';
import { expect, test } from 'vitest';
import { useCounter } from './useCounter';
test('increments counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('decrements counter', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(9);
});
带依赖项的 Hook
import { renderHook, waitFor } from '@testing-library/react';
import { useApi } from './useApi';
test('fetches data successfully', async () => {
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ name: 'John' })
})
) as any;
const { result } = renderHook(() => useApi('/api/user'));
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.data).toEqual({ name: 'John' });
expect(result.current.error).toBeNull();
});
test('handles error', async () => {
global.fetch = vi.fn(() =>
Promise.reject(new Error('Network error'))
);
const { result } = renderHook(() => useApi('/api/user'));
await waitFor(() => {
expect(result.current.error).toBeTruthy();
});
expect(result.current.data).toBeUndefined();
});
集成测试
搭配 Context
import { render, screen } from '@testing-library/react';
import { AuthProvider } from './AuthContext';
import { Dashboard } from './Dashboard';
const renderWithAuth = (ui: ReactElement, { user = null } = {}) => {
return render(
<AuthProvider value={{ user }}>
{ui}
</AuthProvider>
);
};
test('shows dashboard when authenticated', () => {
renderWithAuth(<Dashboard />, { user: { name: 'John' } });
expect(screen.getByText('Welcome, John')).toBeInTheDocument();
});
test('redirects to login when not authenticated', () => {
renderWithAuth(<Dashboard />);
expect(screen.queryByText('Welcome')).not.toBeInTheDocument();
});
搭配 Router
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { App } from './App';
test('navigates to profile page', async () => {
render(
<MemoryRouter initialEntries={['/profile']}>
<App />
</MemoryRouter>
);
expect(screen.getByText('User Profile')).toBeInTheDocument();
});
模拟
API 模拟
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json([
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' }
])
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('displays users from API', async () => {
render(<UserList />);
expect(await screen.findByText('John')).toBeInTheDocument();
expect(await screen.findByText('Jane')).toBeInTheDocument();
});
组件模拟
import { vi } from 'vitest';
vi.mock('./HeavyComponent', () => ({
HeavyComponent: () => <div>Mocked Component</div>
}));
test('renders page with mocked component', () => {
render(<Dashboard />);
expect(screen.getByText('Mocked Component')).toBeInTheDocument();
});
无障碍测试
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('has no accessibility violations', async () => {
const { container } = render(<LoginForm onSubmit={vi.fn()} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
最佳实践
- 使用语义化查询:优先使用
getByRole、getByLabelText,而非getByTestId - 测试用户行为:像用户那样点击、输入和导航
- 避免实现细节:不要直接测试状态或属性
- 异步操作使用
findBy:自动等待元素出现 - 清理环境:使用
afterEach(cleanup)重置 DOM - 模拟外部依赖项:API、计时器和模块
- 测试错误状态:加载、错误和空状态
- 让测试聚焦:尽可能每个测试只进行一项断言
- 使用描述性测试名称:说明预期发生的情况
- 在 CI/CD 中运行测试:部署前确保测试通过
评论
支持 Markdown