AGENT SKILL

frontend/testing-guide

前端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践

License 许可
Apache-2.0
最近更新
2026-11-14
Stars
★ 1.6k
Forks
491

01 前言 Front Matter

name
frontend/testing-guide
type
methodology
agent
boss-frontend
internal
true
metadata
null
description
前端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践

03 SKILL.md 正文

前端测试编写指南

测试要求(强制)

职责边界: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();
  });
});

Hook 测试

// 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'), '<PASSWORD>');
    await userEvent.click(screen.getByRole('button', { name: 'Login' }));
    
    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        email: 'user@example.com',
        password: '<PASSWORD>',
      });
    });
  });
});

集成测试编写

组件交互测试

// 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');
  });
});

E2E 测试编写(必须)

完整 Playwright 方法论:详见 Skill(skill: "qa/e2e-playwright"),包含项目初始化、Page Object Model、认证复用、API Mock、视觉回归、多浏览器测试、CI 集成和调试技巧。

E2E 测试必须覆盖

  • ✅ 创建流程(如:添加数据)
  • ✅ 编辑流程(如:修改数据)
  • ✅ 删除流程(如:删除数据)
  • ✅ 列表展示(如:查看列表)
  • ✅ 核心业务流程

Playwright 示例(Page Object Model)

// 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')

测试隔离

  • 每个测试独立运行,不依赖其他测试
  • 使用 beforeEach 设置初始状态
  • 使用 afterEach 清理副作用

Mock 策略

  • Mock 外部依赖(API、第三方库)
  • 不要 Mock 被测试的代码
  • 使用 MSW(Mock Service Worker)Mock API
// 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' },
      ]),
    );
  }),
];

边界条件测试

  • 空数据:列表为空时的展示
  • 错误状态:API 失败时的处理
  • 加载状态:数据加载中的展示
  • 极限值:最大/最小输入值

无障碍测试

it('is accessible', async () => {
  const { container } = render(<Button>Click</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

测试覆盖率要求

  • 语句覆盖率:≥ 80%
  • 分支覆盖率:≥ 75%
  • 函数覆盖率:≥ 80%
  • 行覆盖率:≥ 80%

运行覆盖率报告:

npm test -- --coverage

测试报告格式

实现完成后,在输出中包含:

测试添加:

类型 文件 描述
单元测试 src/components/Button.test.tsx Button 组件渲染和交互测试
集成测试 src/features/users/UserList.test.tsx 用户列表增删改查集成测试
E2E 测试 e2e/user-management.spec.ts 用户管理完整流程 E2E 测试

测试结果:

  • 通过:25 / 失败:0
  • 覆盖率:85%
  • E2E 测试:✅ 已编写并通过

04 同仓库更多 Skills

hashgraph-online
hashgraph-online/awesome-codex-plugins

migrate-to-e2e

Moves an existing Playwright, Cypress, Selenium, Appium, Detox, Maestro, or TesterArmy suite to e2e one slice at a time, with the old suite running beside the new one until each ported test is proven. Keeps a MIGRATION.md progress file so any later session resumes where the last stopped. Optionally plants a defect to show both suites catch it. Use for /migrate-to-e2e, "migrate our tests to e2e", "port these Maestro flows", "move our TesterArmy tests", or when a project has an e2e config and an older suite still running.

★ 1.6k
hashgraph-online
hashgraph-online/awesome-codex-plugins

e2e

Writes, runs, and debugs end-to-end tests for web and mobile apps with e2e, the agentic test runner, which mixes agent.act goals with exact locator assertions on a Playwright browser or an iOS or Android device. Also drives a project's running app live over the e2e MCP server to look at a screen, check a UI change, take a screenshot or record a video, and try a locator; and runs bug bashes, parallel e2e explore runs with each bug proven by a repro test. Use when a project depends on e2e or has an e2e.config.ts, when asked for end-to-end, browser, mobile, or agentic UI tests, to check a change in the running app, to QA or bug bash an app or branch, or when an e2e run fails.

★ 1.6k
hashgraph-online
hashgraph-online/awesome-codex-plugins

create-verification-skill

Generate a project-local verify-<app> skill on top of e2e, so any coding agent can launch the app, check it is worth driving, exercise a feature the way a user does, keep evidence, and bug bash it. Writes the skill, a feature map with one explore charter per feature, and proves it once. Use for /create-verification-skill, "make a verification skill for this repo", or when a project has no scripted way to prove UI behavior.

★ 1.6k
hashgraph-online
hashgraph-online/awesome-codex-plugins

deja-search

deja-vu memory — search the user's past AI coding sessions with the deja CLI. Holds what they already settled and never wrote into the repo (naming rules, banned or already-tried tools, required checks before merge, preferred values, who owns what). Use before your first edit in a task, before you call a change done or ready to merge, before you recommend a tool, library, design or reviewer, before re-debugging an error that may already be solved, when they say 'didn't we fix this before', 'what did we decide about X' or 'I already have that', and before telling them something on this machine does not exist.

★ 1.6k
hashgraph-online
hashgraph-online/awesome-codex-plugins

deja-reflect

End-of-session reflection and memory curation with deja-vu. Review what was decided, what commands broke and what repaired them, and record durable project rules or decisions with deja remember so the next session starts with them.

★ 1.6k
hashgraph-online
hashgraph-online/awesome-codex-plugins

revid

Make, edit, export and post AI videos with Revid (revid.ai) through the Revid MCP server. Use whenever the user wants a short-form or long-form video made (from an idea, a script, a URL, an article, a PDF, a tweet, a product page, their own clips, or a song), wants to find what is going viral on TikTok, YouTube, Instagram or X before making something, wants to edit or re-export an existing Revid project, generate a standalone AI image or clip, make a series episode, or publish and schedule a finished video to connected social accounts. Covers setup (hosted MCP with OAuth, or an API key), the 36 tools, credits, polling and the rules for publishing. Routes to the more specific revid-* skills when one fits.

图像与多媒体Skill与智能体开发
★ 1.6k