
testing-patterns
by erikpr1994
SKILL.md
name: testing-patterns description: "Vitest, Playwright, and testing strategies. Use when writing tests, setting up test infrastructure, or debugging test failures."
Testing Patterns
Overview
Decision guide for testing strategies focusing on Vitest for unit/integration and Playwright for E2E tests.
Test Type Decision
| Test Type | Tool | Use When |
|---|---|---|
| Unit | Vitest | Pure functions, utilities, hooks |
| Integration | Vitest | Components with dependencies |
| E2E | Playwright | Critical user flows |
| Visual | Playwright | UI regression |
Testing Strategy
Strategy is auto-detected based on project type and can be configured per-directory.
Automatic Strategy Detection
Check current project strategy:
# Via detect.sh
source ~/.claude/lib/testing/strategy-detector.sh
detect_testing_strategy "." # Project-level
detect_directory_strategy "." "src/lib" # Directory-level
Or check settings.json → testing.strategy if explicitly set.
Per-Directory Strategy Rules
| Directory Pattern | Strategy | Rationale |
|---|---|---|
lib/**, packages/**, utils/** | Pyramid | Pure functions, edge cases |
src/components/**, app/** | Trophy | User-facing, integration |
api/**, server/** | Trophy | Test real queries |
algorithms/**, core/** | Pyramid | Complex logic, fast feedback |
Testing Pyramid (Traditional)
/ E2E \ Few - slow, brittle, high confidence
/ Int \ Some - moderate speed/confidence
/ Unit \ Many - fast, isolated, low confidence
Best for: Libraries, utilities, pure logic, microservices
Guidance:
- Test every public function with unit tests
- Cover edge cases extensively (null, empty, boundaries)
- Mock external dependencies
- Aim for >80% unit test coverage
Testing Trophy (Kent C. Dodds)
E2E Few - critical paths only
Integration MOST - best confidence/speed ratio
Unit Some - complex logic only
Static TypeScript, ESLint, etc.
Best for: React apps, user-facing features, full-stack apps
Guidance:
- Test user workflows, not implementation details
- Use Testing Library patterns (query by role, text)
- Mock only network/external services
- Unit test only complex business logic
When to Use Each
| Project Type | Auto-Detected | Reasoning |
|---|---|---|
| UI-heavy app | Trophy | Integration tests catch real user issues |
| Pure library | Pyramid | Unit tests cover edge cases efficiently |
| API/Backend with DB | Trophy | Integration tests verify real queries |
| API/Backend pure | Pyramid | Unit + contract tests are faster |
| Full-stack | Trophy | Integration through API boundaries |
| Monorepo | Balanced | Per-directory strategy applies |
Override Strategy
Project-level - Add to .claude/settings.json:
{
"testing": {
"strategy": "pyramid"
}
}
Directory-level - Add frontmatter to folder's CLAUDE.md:
---
testing_strategy: trophy
---
Vitest Patterns
Basic Test Structure
import { describe, it, expect, beforeEach, vi } from 'vitest';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
vi.clearAllMocks();
});
it('creates user with valid data', async () => {
const user = await service.create({ email: 'test@example.com' });
expect(user).toMatchObject({ email: 'test@example.com' });
});
it('throws on duplicate email', async () => {
await service.create({ email: 'test@example.com' });
await expect(service.create({ email: 'test@example.com' }))
.rejects.toThrow('already exists');
});
});
Mocking
// Mock module
vi.mock('@/lib/db', () => ({
db: {
user: {
create: vi.fn(),
findUnique: vi.fn(),
},
},
}));
// Mock implementation per test
it('handles not found', async () => {
vi.mocked(db.user.findUnique).mockResolvedValue(null);
await expect(getUser('123')).rejects.toThrow('Not found');
});
// Spy on existing function
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(spy).toHaveBeenCalledWith('Error message');
React Component Testing
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
it('submits form with valid data', async () => {
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'password123');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
Playwright Patterns
Page Object Model
// tests/pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.fill('[name="email"]', email);
await this.page.fill('[name="password"]', password);
await this.page.click('button[type="submit"]');
}
async expectError(message: string) {
await expect(this.page.getByText(message)).toBeVisible();
}
}
// tests/auth.spec.ts
test('user can login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
});
Fixtures
// tests/fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/login.page';
type Fixtures = {
loginPage: LoginPage;
authenticatedPage: Page;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
await use(page);
},
});
Running Long E2E Tests Efficiently
E2E tests often take 2-5+ minutes. Use the TaskOutput pattern to avoid token waste.
The Problem: Polling Waste
❌ INEFFICIENT - wastes tokens on empty polling:
1. Bash(test, run_in_background: true) → task_id
2. Bash(sleep 60 && tail output) ← empty, wasted tokens
3. Bash(cat output) ← still empty, wasted
4. Bash(ps aux | grep test) ← check if running, wasted
5. Bash(sleep 90 && cat output) ← wasted again
... repeat 5-10 times before getting results
The Solution: Block Until Complete
✅ EFFICIENT - one call, waits for completion:
1. Bash(test, run_in_background: true) → task_id
2. TaskOutput(task_id, block: true, timeout: 300000)
↳ Waits up to 5 min, returns results when done
That's it. Two tool calls instead of 6+.
Pattern: E2E Test Execution
// Step 1: Start test in background
Bash("pnpm test:e2e -- e2e/checkout.spec.ts", run_in_background: true)
// Returns: task_id = "abc123"
// Step 2: Wait for completion (up to 5 min)
TaskOutput(task_id: "abc123", block: true, timeout: 300000)
// Returns full output when test completes
Pattern: Parallel E2E with TaskOutput
// Start multiple tests in parallel
Bash("pnpm test:e2e auth.spec.ts", run_in_background: true) // task_a
Bash("pnpm test:e2e cart.spec.ts", run_in_background: true) // task_b
Bash("pnpm test:e2e checkout.spec.ts", run_in_background: true) // task_c
// Wait for all to complete
TaskOutput(task_id: "task_a", block: true, timeout: 300000)
TaskOutput(task_id: "task_b", block: true, timeout: 300000)
TaskOutput(task_id: "task_c", block: true, timeout: 300000)
When to Use Non-Blocking
Use block: false only when you need a quick status check:
// Quick status check (doesn't wait)
TaskOutput(task_id: "abc123", block: false, timeout: 5000)
// Returns immediately with current output
// Use this if you want to continue other work
Timeout Guidelines
| Test Type | Suggested Timeout |
|---|---|
| Single unit test file | 60000 (1 min) |
| Integration tests | 120000 (2 min) |
| E2E single spec | 300000 (5 min) |
| E2E full suite | 600000 (10 min) |
Red Flags
- Using
sleep && tailto poll → Use TaskOutput instead - Multiple
catcommands checking same file → Use TaskOutput with block: true - Checking
ps aux | grepfor process → TaskOutput tells you if still running
Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Testing implementation | Brittle tests | Test behavior/output |
| No test isolation | Flaky tests | Reset state in beforeEach |
| Hardcoded delays | Slow, flaky | Use waitFor/polling |
| Testing third-party code | Wasted effort | Mock at boundary |
| Snapshot abuse | Meaningless diffs | Use for specific UI |
| Polling background tasks | Token waste | Use TaskOutput(block: true) |
// BAD: Testing implementation
expect(component.state.isLoading).toBe(true);
// GOOD: Testing behavior
expect(screen.getByRole('status')).toHaveTextContent('Loading...');
// BAD: Hardcoded delay
await page.waitForTimeout(2000);
// GOOD: Wait for condition
await page.waitForSelector('[data-loaded="true"]');
Test Organization
tests/
├── unit/ # Pure function tests
├── integration/ # Component + dependency tests
├── e2e/ # Playwright tests
│ ├── fixtures/
│ ├── pages/ # Page objects
│ └── *.spec.ts
└── setup.ts # Global setup
Red Flags
- Tests that pass when code is broken
- Tests that fail intermittently (flaky)
- Tests longer than 50 lines (decompose)
- Mocking everything (test becomes meaningless)
- No assertions (test does nothing)
- Duplicated setup across tests (use fixtures)
Quick Reference
// Vitest config
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
coverage: { reporter: ['text', 'html'] },
},
});
// Playwright config
export default defineConfig({
testDir: './tests/e2e',
use: { baseURL: 'http://localhost:3000' },
webServer: { command: 'npm run dev', port: 3000 },
});
// Fast feedback: watch mode
vitest --watch
playwright test --ui
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です