--- name: tester-expert description: > 测试专家。当用户需要编写单元测试、集成测试、E2E 端到端测试、TDD 测试驱动开发、 Jest/Vitest/Playwright/Cypress/pytest 测试框架、Mock/Stub、测试覆盖率, 或说 "写测试"、"测试用例" 时使用此技能。 allowed-tools: Read, Glob, Grep, Edit, Write, Bash, mcp__playwright, mcp__chrome-devtools, mcp__selenium maturity: stable last-reviewed: 2026-02-18 composable: true enhances: [debugger-expert, zero-defect-guardian] --- # 测试专家 (Tester Expert) > **Output Style**: 本技能使用内联输出规范 资深测试工程师,精通各种测试策略、测试框架和 TDD 开发方法。 ## 触发关键词 - **core tier**: `单元测试`, `集成测试`, `E2E测试`, `端到端测试`, `写测试`, `测试用例`, `unit test`, `integration test`, `write tests`, `test case`, `test coverage`, `testing` - **strong tier**: `Jest`, `Vitest`, `Playwright`, `Cypress`, `pytest`, `TDD`, `BDD`, `测试覆盖率`, `test suite`, `test runner`, `component test`, `snapshot test`, `regression test` - **extended tier**: `测试驱动`, `Mock`, `Stub`, `覆盖率`, `代码覆盖`, `测试方案`, `testing library`, `test driven`, `assertion` - **排除场景**: `A/B测试`(数据实验)→ 路由至 data-analyst-expert;`pandas` 相关测试 → 路由至 data-analyst-expert ## 测试金字塔 ``` /\ / \ / E2E \ 少量:关键用户流程 /______\ / \ /Integration\ 中量:API和服务交互 /______________\ / \ / Unit Tests \ 大量:函数和模块 /____________________\ ``` ## 单元测试 ### Jest / Vitest ```typescript // user.service.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import { UserService } from './user.service'; describe('UserService', () => { let userService: UserService; let mockUserRepository: any; beforeEach(() => { mockUserRepository = { findById: vi.fn(), save: vi.fn(), }; userService = new UserService(mockUserRepository); }); describe('getUserById', () => { it('should return user when found', async () => { const mockUser = { id: '1', name: 'John' }; mockUserRepository.findById.mockResolvedValue(mockUser); const result = await userService.getUserById('1'); expect(result).toEqual(mockUser); expect(mockUserRepository.findById).toHaveBeenCalledWith('1'); }); it('should return null when user not found', async () => { mockUserRepository.findById.mockResolvedValue(null); const result = await userService.getUserById('999'); expect(result).toBeNull(); }); it('should throw error on database failure', async () => { mockUserRepository.findById.mockRejectedValue(new Error('DB Error')); await expect(userService.getUserById('1')).rejects.toThrow('DB Error'); }); }); }); ``` ### React 组件测试 ```typescript // Button.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { Button } from './Button'; describe('Button', () => { it('renders with text', () => { render(); expect(screen.getByText('Click me')).toBeInTheDocument(); }); it('calls onClick when clicked', () => { const handleClick = vi.fn(); render(); fireEvent.click(screen.getByRole('button')); expect(handleClick).toHaveBeenCalledTimes(1); }); it('shows loading spinner when loading', () => { render(); expect(screen.getByRole('progressbar')).toBeInTheDocument(); expect(screen.queryByText('Submit')).not.toBeInTheDocument(); }); it('is disabled when disabled prop is true', () => { render(); expect(screen.getByRole('button')).toBeDisabled(); }); }); ``` ## 集成测试 ```typescript // api.integration.test.ts import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import request from 'supertest'; import { app } from '../app'; import { db } from '../db'; describe('User API', () => { beforeAll(async () => { await db.migrate.latest(); await db.seed.run(); }); afterAll(async () => { await db.destroy(); }); describe('GET /api/users', () => { it('should return list of users', async () => { const response = await request(app) .get('/api/users') .expect(200); expect(response.body).toHaveProperty('users'); expect(Array.isArray(response.body.users)).toBe(true); }); }); describe('POST /api/users', () => { it('should create a new user', async () => { const newUser = { name: 'Test User', email: 'test@example.com' }; const response = await request(app) .post('/api/users') .send(newUser) .expect(201); expect(response.body.user.name).toBe(newUser.name); expect(response.body.user.email).toBe(newUser.email); }); it('should return 400 for invalid data', async () => { const invalidUser = { name: '' }; await request(app) .post('/api/users') .send(invalidUser) .expect(400); }); }); }); ``` ## E2E 测试 ### Playwright ```typescript // login.spec.ts import { test, expect } from '@playwright/test'; test.describe('Login Flow', () => { test('should login successfully with valid credentials', async ({ page }) => { await page.goto('/login'); await page.fill('[data-testid="email"]', 'user@example.com'); await page.fill('[data-testid="password"]', 'password123'); await page.click('[data-testid="submit"]'); await expect(page).toHaveURL('/dashboard'); await expect(page.locator('[data-testid="welcome-message"]')).toBeVisible(); }); test('should show error for invalid credentials', async ({ page }) => { await page.goto('/login'); await page.fill('[data-testid="email"]', 'wrong@example.com'); await page.fill('[data-testid="password"]', 'wrongpassword'); await page.click('[data-testid="submit"]'); await expect(page.locator('[data-testid="error-message"]')).toBeVisible(); await expect(page).toHaveURL('/login'); }); }); ``` ## TDD 流程 ``` 1. Red: 写一个失败的测试 2. Green: 写最少代码让测试通过 3. Refactor: 重构代码,保持测试通过 ``` ## 测试覆盖率配置 ```javascript // vitest.config.ts export default defineConfig({ test: { coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'], exclude: ['node_modules/', 'test/'], thresholds: { lines: 80, functions: 80, branches: 80, statements: 80, }, }, }, }); ``` ## 测试命名规范 ```typescript // 格式: should [expected behavior] when [condition] it('should return empty array when no users exist'); it('should throw error when id is invalid'); it('should update user name when valid name provided'); ``` ## 输出规范 - 测试代码完整可运行 - 覆盖正常情况和边界情况 - 使用清晰的测试描述 - 遵循 AAA 模式 (Arrange, Act, Assert) ## 并发测试与竞态检测 ### 竞态条件测试模板 ```typescript // race-condition.test.ts — 并发读写竞态检测 import { describe, it, expect } from 'vitest'; describe('Concurrency Safety', () => { it('should handle concurrent writes without data loss', async () => { const results: number[] = []; const counter = { value: 0 }; // 模拟 N 个并发写入 const N = 100; const promises = Array.from({ length: N }, (_, i) => Promise.resolve().then(() => { const current = counter.value; // 模拟异步间隙 (竞态窗口) counter.value = current + 1; results.push(counter.value); }) ); await Promise.all(promises); // 如果存在竞态,counter.value < N expect(counter.value).toBe(N); }); it('should not deadlock under concurrent lock acquisition', async () => { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Deadlock detected: timeout')), 5000) ); const operation = runConcurrentLockTest(); // 被测函数 // 5 秒内必须完成,否则判定为死锁 await expect(Promise.race([operation, timeout])).resolves.toBeDefined(); }); }); ``` ### 文件锁竞态测试 ```typescript // file-lock-race.test.ts it('concurrent file writes should not corrupt JSON', async () => { const file = path.join(tmpDir, 'test.json'); fs.writeFileSync(file, JSON.stringify({ count: 0 })); // 10 个并发进程同时 read-modify-write const workers = Array.from({ length: 10 }, () => new Promise((resolve) => { const data = JSON.parse(fs.readFileSync(file, 'utf8')); data.count++; const tmp = file + '.tmp.' + Math.random(); fs.writeFileSync(tmp, JSON.stringify(data)); fs.renameSync(tmp, file); resolve(); }) ); await Promise.all(workers); // 验证: 无锁时 count < 10 (竞态丢失) const final = JSON.parse(fs.readFileSync(file, 'utf8')); // 有锁保护时应 === 10 expect(final.count).toBeLessThanOrEqual(10); }); ``` ### 并发测试检查清单 - [ ] 共享状态读写是否有锁保护? - [ ] Promise.all 中的操作是否互相独立? - [ ] 数据库事务隔离级别是否足够? - [ ] 文件操作是否使用 temp+rename 原子写入? - [ ] 计数器/ID 生成是否原子操作? - [ ] 缓存失效时的 thundering herd 是否处理? ## 禁止事项 - ❌ 不要测试实现细节 - ❌ 不要忽略边界情况 - ❌ 不要写互相依赖的测试 - ❌ 不要忽略异步错误处理 - ❌ 不要跳过并发场景测试 (多用户/多进程操作同一资源) ## 突变测试 (Mutation Testing) ### 概念 通过微小修改源代码 (突变体) 验证现有测试是否能检测到变化。如果测试仍通过 → 测试无效。 ### 手动突变检查清单 对关键函数,逐一验证以下突变是否被测试捕获: - [ ] `>` 改为 `>=` (边界条件) - [ ] `&&` 改为 `||` (逻辑反转) - [ ] `+1` 改为 `-1` (off-by-one) - [ ] `return true` 改为 `return false` (返回值反转) - [ ] 删除一个 if 分支 (条件删除) - [ ] 交换两个参数顺序 (参数置换) ### Stryker (JS/TS 突变测试工具) ```bash # 安装 npx stryker init # 运行 npx stryker run # 报告解读 # Mutation Score = killed / (killed + survived) # 目标: >80% (核心逻辑 >90%) ``` ### 突变测试适用场景 - 金额计算、权限判断、状态机转换等高风险逻辑 - 测试覆盖率高但信心不足时 - 不适用于 UI 组件、配置文件等