addyosmani--agent-skills
fea75b1647
Update all SKILL.md descriptions to lead with what the skill does (third person) followed by trigger conditions, per Anthropic's skill authoring docs. All original trigger conditions preserved. Update description max chars to match Anthropic spec (1024). Retain workflow-summary warning in skill-anatomy.md to prevent agents from following descriptions instead of reading full skills. Add Table of Contents to reference files longer than 100 lines so Claude can see the full scope of available information even when previewing with partial reads.
237 行
6.8 KiB
Markdown
237 行
6.8 KiB
Markdown
# Testing Patterns Reference
|
|
|
|
Quick reference for common testing patterns across the stack. Use alongside the `test-driven-development` skill.
|
|
|
|
## Table of Contents
|
|
|
|
- [Test Structure (Arrange-Act-Assert)](#test-structure-arrange-act-assert)
|
|
- [Test Naming Conventions](#test-naming-conventions)
|
|
- [Common Assertions](#common-assertions)
|
|
- [Mocking Patterns](#mocking-patterns)
|
|
- [React/Component Testing](#reactcomponent-testing)
|
|
- [API / Integration Testing](#api--integration-testing)
|
|
- [E2E Testing (Playwright)](#e2e-testing-playwright)
|
|
- [Test Anti-Patterns](#test-anti-patterns)
|
|
|
|
## Test Structure (Arrange-Act-Assert)
|
|
|
|
```typescript
|
|
it('describes expected behavior', () => {
|
|
// Arrange: Set up test data and preconditions
|
|
const input = { title: 'Test Task', priority: 'high' };
|
|
|
|
// Act: Perform the action being tested
|
|
const result = createTask(input);
|
|
|
|
// Assert: Verify the outcome
|
|
expect(result.title).toBe('Test Task');
|
|
expect(result.priority).toBe('high');
|
|
expect(result.status).toBe('pending');
|
|
});
|
|
```
|
|
|
|
## Test Naming Conventions
|
|
|
|
```typescript
|
|
// Pattern: [unit] [expected behavior] [condition]
|
|
describe('TaskService.createTask', () => {
|
|
it('creates a task with default pending status', () => {});
|
|
it('throws ValidationError when title is empty', () => {});
|
|
it('trims whitespace from title', () => {});
|
|
it('generates a unique ID for each task', () => {});
|
|
});
|
|
```
|
|
|
|
## Common Assertions
|
|
|
|
```typescript
|
|
// Equality
|
|
expect(result).toBe(expected); // Strict equality (===)
|
|
expect(result).toEqual(expected); // Deep equality (objects/arrays)
|
|
expect(result).toStrictEqual(expected); // Deep equality + type matching
|
|
|
|
// Truthiness
|
|
expect(result).toBeTruthy();
|
|
expect(result).toBeFalsy();
|
|
expect(result).toBeNull();
|
|
expect(result).toBeDefined();
|
|
expect(result).toBeUndefined();
|
|
|
|
// Numbers
|
|
expect(result).toBeGreaterThan(5);
|
|
expect(result).toBeLessThanOrEqual(10);
|
|
expect(result).toBeCloseTo(0.3, 5); // Floating point
|
|
|
|
// Strings
|
|
expect(result).toMatch(/pattern/);
|
|
expect(result).toContain('substring');
|
|
|
|
// Arrays / Objects
|
|
expect(array).toContain(item);
|
|
expect(array).toHaveLength(3);
|
|
expect(object).toHaveProperty('key', 'value');
|
|
|
|
// Errors
|
|
expect(() => fn()).toThrow();
|
|
expect(() => fn()).toThrow(ValidationError);
|
|
expect(() => fn()).toThrow('specific message');
|
|
|
|
// Async
|
|
await expect(asyncFn()).resolves.toBe(value);
|
|
await expect(asyncFn()).rejects.toThrow(Error);
|
|
```
|
|
|
|
## Mocking Patterns
|
|
|
|
### Mock Functions
|
|
|
|
```typescript
|
|
const mockFn = jest.fn();
|
|
mockFn.mockReturnValue(42);
|
|
mockFn.mockResolvedValue({ data: 'test' });
|
|
mockFn.mockImplementation((x) => x * 2);
|
|
|
|
expect(mockFn).toHaveBeenCalled();
|
|
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
|
|
expect(mockFn).toHaveBeenCalledTimes(3);
|
|
```
|
|
|
|
### Mock Modules
|
|
|
|
```typescript
|
|
// Mock an entire module
|
|
jest.mock('./database', () => ({
|
|
query: jest.fn().mockResolvedValue([{ id: 1, title: 'Test' }]),
|
|
}));
|
|
|
|
// Mock specific exports
|
|
jest.mock('./utils', () => ({
|
|
...jest.requireActual('./utils'),
|
|
generateId: jest.fn().mockReturnValue('test-id'),
|
|
}));
|
|
```
|
|
|
|
### Mock at Boundaries Only
|
|
|
|
```
|
|
Mock these: Don't mock these:
|
|
├── Database calls ├── Internal utility functions
|
|
├── HTTP requests ├── Business logic
|
|
├── File system operations ├── Data transformations
|
|
├── External API calls ├── Validation functions
|
|
└── Time/Date (when needed) └── Pure functions
|
|
```
|
|
|
|
## React/Component Testing
|
|
|
|
```tsx
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
|
|
describe('TaskForm', () => {
|
|
it('submits the form with entered data', async () => {
|
|
const onSubmit = jest.fn();
|
|
render(<TaskForm onSubmit={onSubmit} />);
|
|
|
|
// Find elements by accessible role/label (not test IDs)
|
|
await screen.findByRole('textbox', { name: /title/i });
|
|
fireEvent.change(screen.getByRole('textbox', { name: /title/i }), {
|
|
target: { value: 'New Task' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: /create/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(onSubmit).toHaveBeenCalledWith({ title: 'New Task' });
|
|
});
|
|
});
|
|
|
|
it('shows validation error for empty title', async () => {
|
|
render(<TaskForm onSubmit={jest.fn()} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /create/i }));
|
|
|
|
expect(await screen.findByText(/title is required/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
```
|
|
|
|
## API / Integration Testing
|
|
|
|
```typescript
|
|
import request from 'supertest';
|
|
import { app } from '../src/app';
|
|
|
|
describe('POST /api/tasks', () => {
|
|
it('creates a task and returns 201', async () => {
|
|
const response = await request(app)
|
|
.post('/api/tasks')
|
|
.send({ title: 'Test Task' })
|
|
.set('Authorization', `Bearer ${testToken}`)
|
|
.expect(201);
|
|
|
|
expect(response.body).toMatchObject({
|
|
id: expect.any(String),
|
|
title: 'Test Task',
|
|
status: 'pending',
|
|
});
|
|
});
|
|
|
|
it('returns 422 for invalid input', async () => {
|
|
const response = await request(app)
|
|
.post('/api/tasks')
|
|
.send({ title: '' })
|
|
.set('Authorization', `Bearer ${testToken}`)
|
|
.expect(422);
|
|
|
|
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
|
|
it('returns 401 without authentication', async () => {
|
|
await request(app)
|
|
.post('/api/tasks')
|
|
.send({ title: 'Test' })
|
|
.expect(401);
|
|
});
|
|
});
|
|
```
|
|
|
|
## E2E Testing (Playwright)
|
|
|
|
```typescript
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
test('user can create and complete a task', async ({ page }) => {
|
|
// Navigate and authenticate
|
|
await page.goto('/');
|
|
await page.fill('[name="email"]', 'test@example.com');
|
|
await page.fill('[name="password"]', 'testpass123');
|
|
await page.click('button:has-text("Log in")');
|
|
|
|
// Create a task
|
|
await page.click('button:has-text("New Task")');
|
|
await page.fill('[name="title"]', 'Buy groceries');
|
|
await page.click('button:has-text("Create")');
|
|
|
|
// Verify task appears
|
|
await expect(page.locator('text=Buy groceries')).toBeVisible();
|
|
|
|
// Complete the task
|
|
await page.click('[aria-label="Complete Buy groceries"]');
|
|
await expect(page.locator('text=Buy groceries')).toHaveCSS(
|
|
'text-decoration-line', 'line-through'
|
|
);
|
|
});
|
|
```
|
|
|
|
## Test Anti-Patterns
|
|
|
|
| Anti-Pattern | Problem | Better Approach |
|
|
|---|---|---|
|
|
| Testing implementation details | Breaks on refactor | Test inputs/outputs |
|
|
| Snapshot everything | No one reviews snapshot diffs | Assert specific values |
|
|
| Shared mutable state | Tests pollute each other | Setup/teardown per test |
|
|
| Testing third-party code | Wastes time, not your bug | Mock the boundary |
|
|
| Skipping tests to pass CI | Hides real bugs | Fix or delete the test |
|
|
| Using `test.skip` permanently | Dead code | Remove or fix it |
|
|
| Overly broad assertions | Doesn't catch regressions | Be specific |
|
|
| No async error handling | Swallowed errors, false passes | Always `await` async tests |
|