بنقرة واحدة
frontend-testing-guide
前端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
前端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
BMAD 全自动研发流水线编排器。编排 9 个专业 Agent(PM、架构师、UI Designer、Tech Lead、Scrum Master、Frontend、Backend、QA、DevOps)从需求到部署。 Triggers: 'boss mode', '/boss', '全自动开发', '从需求到部署', '帮我做一个', 'build this', 'ship it', '全流程', '自动化开发', '一键开发', 'start a project', 'new feature' Does NOT trigger: - 单文件修改或简单 bug 修复(直接编辑即可) - 纯代码阅读或解释(使用 read 工具) - 已有 pipeline 正在运行时的重复启动 - 极小事(预计 <30 分钟人工可完成、不需要 PRD/架构/门禁记录) Output: 完整项目代码 + PRD/架构/UI/测试/部署文档,写入 .boss/<feature>/ 目录
后端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
自动生成 CHANGELOG,基于 git 提交历史和 pipeline 产物信息,遵循 Conventional Commits 和 Keep a Changelog 规范
从CEO/战略视角进行商业价值评审,评估市场契合度、ROI、竞争优势、风险和战略对齐
设计变体模式,产出2-3个设计方案及 tradeoff 分析,供用户选择后确定最终方案
Playwright E2E 测试完整方法论,涵盖项目初始化、Page Object Model、认证复用、API Mock、视觉回归、多浏览器测试、CI 集成和调试技巧
| name | frontend/testing-guide |
| description | 前端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践 |
| type | methodology |
| agent | boss-frontend |
职责边界:Frontend Agent 是测试的编写者,QA Agent 是测试的验证者。
| 测试类型 | 占比 | 要求 |
|---|---|---|
| 单元测试 | ~70% | 每个组件/Hook 必须有测试 |
| 集成测试 | ~20% | 组件交互、状态管理测试 |
| E2E 测试 | ~10% | 必须编写,覆盖用户流程 |
// Button.test.tsx
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
screen.getByText('Click').click();
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click</Button>);
expect(screen.getByText('Click')).toBeDisabled();
});
});
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
// LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('shows validation error for invalid email', async () => {
render(<LoginForm />);
const emailInput = screen.getByLabelText('Email');
await userEvent.type(emailInput, 'invalid-email');
await userEvent.tab(); // Trigger blur
await waitFor(() => {
expect(screen.getByText('Invalid email format')).toBeInTheDocument();
});
});
it('submits form with valid data', async () => {
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText('Email'), 'user@example.com');
await userEvent.type(screen.getByLabelText('Password'), 'password123');
await userEvent.click(screen.getByRole('button', { name: 'Login' }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123',
});
});
});
});
// UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserList } from './UserList';
import { UserProvider } from './UserContext';
describe('UserList integration', () => {
it('adds new user to list', async () => {
render(
<UserProvider>
<UserList />
</UserProvider>
);
// 打开添加用户表单
await userEvent.click(screen.getByText('Add User'));
// 填写表单
await userEvent.type(screen.getByLabelText('Name'), 'John Doe');
await userEvent.type(screen.getByLabelText('Email'), 'john@example.com');
// 提交
await userEvent.click(screen.getByRole('button', { name: 'Submit' }));
// 验证用户出现在列表中
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
});
});
// store.test.ts
import { renderHook, act } from '@testing-library/react';
import { useStore } from './store';
describe('Store integration', () => {
it('updates user state across components', () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.setUser({ id: '1', name: 'Alice' });
});
expect(result.current.user).toEqual({ id: '1', name: 'Alice' });
act(() => {
result.current.updateUserName('Bob');
});
expect(result.current.user?.name).toBe('Bob');
});
});
完整 Playwright 方法论:详见
Skill(skill: "qa/e2e-playwright"),包含项目初始化、Page Object Model、认证复用、API Mock、视觉回归、多浏览器测试、CI 集成和调试技巧。
// e2e/pages/user-list.page.ts
import { type Page, type Locator } from '@playwright/test';
export class UserListPage {
private readonly addButton: Locator;
private readonly table: Locator;
constructor(private readonly page: Page) {
this.addButton = page.getByRole('button', { name: '添加用户' });
this.table = page.getByRole('table');
}
async goto() { await this.page.goto('/users'); }
async clickAddUser() { await this.addButton.click(); }
getTable() { return this.table; }
async editUser(name: string) {
await this.page.getByRole('row', { name }).getByRole('button', { name: '编辑' }).click();
}
async deleteUser(name: string) {
await this.page.getByRole('row', { name }).getByRole('button', { name: '删除' }).click();
}
async confirmDelete() {
await this.page.getByRole('button', { name: '确认' }).click();
}
}
// e2e/specs/crud/user-management.spec.ts
import { test, expect } from '@playwright/test';
import { UserListPage } from '../../pages/user-list.page';
test.describe('用户管理 CRUD', () => {
let userList: UserListPage;
test.beforeEach(async ({ page }) => {
userList = new UserListPage(page);
await userList.goto();
});
test('创建 → 编辑 → 删除完整流程', async ({ page }) => {
// 创建
await userList.clickAddUser();
await page.getByLabel('姓名').fill('测试用户');
await page.getByLabel('邮箱').fill('test@example.com');
await page.getByRole('button', { name: '提交' }).click();
await expect(page.getByText('测试用户')).toBeVisible();
// 编辑
await userList.editUser('测试用户');
await page.getByLabel('姓名').fill('修改后的用户');
await page.getByRole('button', { name: '提交' }).click();
await expect(page.getByText('修改后的用户')).toBeVisible();
// 删除
await userList.deleteUser('修改后的用户');
await userList.confirmDelete();
await expect(page.getByText('修改后的用户')).not.toBeVisible();
});
test('列表分页展示', async ({ page }) => {
await expect(userList.getTable()).toBeVisible();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page).toHaveURL(/page=2/);
});
});
| 优先级 | 方法 | 说明 |
|---|---|---|
| 1 | getByRole | 无障碍语义,最稳定 |
| 2 | getByLabel | 表单元素首选 |
| 3 | getByText | 静态文本 |
| 4 | getByTestId | 无语义标记时兜底 |
| 5 | CSS/XPath | 尽量避免 |
it('should [expected behavior] when [condition]')it('should show error message when email is invalid')// mocks/handlers.ts
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json([
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
])
);
}),
];
it('is accessible', async () => {
const { container } = render(<Button>Click</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
运行覆盖率报告:
npm test -- --coverage
实现完成后,在输出中包含:
测试添加:
| 类型 | 文件 | 描述 |
|---|---|---|
| 单元测试 | src/components/Button.test.tsx | Button 组件渲染和交互测试 |
| 集成测试 | src/features/users/UserList.test.tsx | 用户列表增删改查集成测试 |
| E2E 测试 | e2e/user-management.spec.ts | 用户管理完整流程 E2E 测试 |
测试结果: