Comprehensive testing strategies including test pyramid, TDD methodology, testing patterns, coverage goals, and CI/CD integration. Use when writing tests, implementing TDD, reviewing test coverage, debugging test failures, or setting up testing infrastructure.
This skill provides comprehensive guidance for implementing effective testing strategies across your entire application stack.
/\
/ \
/E2E \ 10% - End-to-End Tests (slowest, most expensive)
/______\
/ \
/Integration\ 20% - Integration Tests (medium speed/cost)
/____________\
/ \
/ Unit Tests \ 70% - Unit Tests (fast, cheap, focused)
/__________________\
Rationale:
Unit tests are cheap:
Integration tests are moderate:
E2E tests are expensive:
1. Red - Write a failing test:
describe('Calculator', () => {
test('adds two numbers', () => {
const calculator = new Calculator();
expect(calculator.add(2, 3)).toBe(5); // FAILS - method doesn't exist
});
});
2. Green - Write minimal code to pass:
class Calculator {
add(a: number, b: number): number {
return a + b; // Simplest implementation
}
}
// Test now PASSES
3. Refactor - Improve the code:
class Calculator {
add(a: number, b: number): number {
// Add validation
if (!Number.isFinite(a) || !Number.isFinite(b)) {
throw new Error('Arguments must be finite numbers');
}
return a + b;
}
}
Design benefits:
Quality benefits:
Workflow benefits:
Every test should follow this structure:
test('user registration creates account and sends welcome email', async () => {
// ARRANGE - Set up test conditions
const userData = {
email: 'test@example.com',
password: 'SecurePass123',
name: 'Test User',
};
const mockEmailService = jest.fn();
const userService = new UserService(mockEmailService);
// ACT - Execute the behavior being tested
const result = await userService.register(userData);
// ASSERT - Verify the outcome
expect(result.id).toBeDefined();
expect(result.email).toBe(userData.email);
expect(mockEmailService).toHaveBeenCalledWith({
to: userData.email,
subject: 'Welcome!',
template: 'welcome',
});
});
✅ DO mock:
// Mock external API
jest.mock('axios');
test('fetches user data from API', async () => {
const mockData = { id: 1, name: 'John' };
(axios.get as jest.Mock).mockResolvedValue({ data: mockData });
const user = await fetchUser(1);
expect(user).toEqual(mockData);
});
❌ DON'T mock:
// ❌ BAD - Over-mocking
test('validates email', () => {
const validator = new EmailValidator();
jest.spyOn(validator, 'isValid').mockReturnValue(true);
expect(validator.isValid('test@example.com')).toBe(true);
// This test is useless - you're testing the mock, not the code
});
// ✅ GOOD - Test real implementation
test('validates email', () => {
const validator = new EmailValidator();
expect(validator.isValid('test@example.com')).toBe(true);
expect(validator.isValid('invalid')).toBe(false);
});
Stub (return predetermined values):
const mockDatabase = {
findUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' }),
saveUser: jest.fn().mockResolvedValue(true),
};
Spy (track calls, use real implementation):
const emailService = new EmailService();
const sendSpy = jest.spyOn(emailService, 'send');
await emailService.send('test@example.com', 'Hello');
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(sendSpy).toHaveBeenCalledWith('test@example.com', 'Hello');
Fake (lightweight implementation):
class FakeDatabase {
private data = new Map();
async save(key: string, value: any) {
this.data.set(key, value);
}
async get(key: string) {
return this.data.get(key);
}
}
Line Coverage: Percentage of code lines executed
Branch Coverage: Percentage of if/else branches tested
Function Coverage: Percentage of functions called
Statement Coverage: Percentage of statements executed
// package.json
{
"jest": {
"collectCoverage": true,
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 90,
"lines": 80,
"statements": 80
},
"./src/critical/": {
"branches": 95,
"functions": 95,
"lines": 95,
"statements": 95
}
},
"coveragePathIgnorePatterns": [
"/node_modules/",
"/tests/",
"/migrations/",
"/.config.ts$/"
]
}
}
High priority (aim for 95%+ coverage):
Medium priority (aim for 80%+ coverage):
Low priority (optional coverage):
import { PrismaClient } from '@prisma/client';
describe('UserRepository', () => {
let prisma: PrismaClient;
let repository: UserRepository;
beforeAll(async () => {
// Use test database
prisma = new PrismaClient({
datasources: { db: { url: process.env.TEST_DATABASE_URL } },
});
repository = new UserRepository(prisma);
});
beforeEach(async () => {
// Clean database before each test
await prisma.user.deleteMany();
});
afterAll(async () => {
await prisma.$disconnect();
});
test('creates user and retrieves by email', async () => {
// ARRANGE
const userData = {
email: 'test@example.com',
name: 'Test User',
password: 'hashed_password',
};
// ACT
const created = await repository.create(userData);
const retrieved = await repository.findByEmail(userData.email);
// ASSERT
expect(retrieved).toBeDefined();
expect(retrieved?.id).toBe(created.id);
expect(retrieved?.email).toBe(userData.email);
});
});
import request from 'supertest';
import { app } from '../src/app';
describe('User API', () => {
test('POST /api/users creates user and returns 201', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
password: 'SecurePass123',
name: 'Test User',
})
.expect(201);
expect(response.body).toMatchObject({
email: 'test@example.com',
name: 'Test User',
});
expect(response.body.password).toBeUndefined(); // Never return password
});
test('POST /api/users returns 400 for invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
password: 'SecurePass123',
name: 'Test User',
})
.expect(400);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
});
describe('OrderService Integration', () => {
test('complete order flow', async () => {
// Create order
const order = await orderService.create({
userId: 'user_123',
items: [{ productId: 'prod_1', quantity: 2 }],
});
// Process payment
const payment = await paymentService.process({
orderId: order.id,
amount: order.total,
});
// Verify inventory updated
const product = await inventoryService.getProduct('prod_1');
expect(product.stock).toBe(originalStock - 2);
// Verify order status updated
const updatedOrder = await orderService.getById(order.id);
expect(updatedOrder.status).toBe('paid');
});
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
{ name: 'firefox', use: { browserName: 'firefox' } },
{ name: 'webkit', use: { browserName: 'webkit' } },
],
});
import { test, expect } from '@playwright/test';
test.describe('User Registration Flow', () => {
test('user can register and login', async ({ page }) => {
// Navigate to registration page
await page.goto('/register');
// Fill registration form
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'SecurePass123');
await page.fill('[name="confirmPassword"]', 'SecurePass123');
await page.fill('[name="name"]', 'Test User');
// Submit form
await page.click('button[type="submit"]');
// Wait for redirect to dashboard
await page.waitForURL('/dashboard');
// Verify welcome message
await expect(page.locator('h1')).toContainText('Welcome, Test User');
});
test('shows validation errors for invalid input', async ({ page }) => {
await page.goto('/register');
await page.fill('[name="email"]', 'invalid-email');
await page.fill('[name="password"]', '123'); // Too short
await page.click('button[type="submit"]');
// Verify error messages displayed
await expect(page.locator('[data-testid="email-error"]'))
.toContainText('Invalid email');
await expect(page.locator('[data-testid="password-error"]'))
.toContainText('at least 8 characters');
});
});
Test these critical user journeys:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp up to 20 users
{ duration: '1m', target: 20 }, // Stay at 20 users
{ duration: '30s', target: 100 }, // Ramp up to 100 users
{ duration: '1m', target: 100 }, // Stay at 100 users
{ duration: '30s', target: 0 }, // Ramp down to 0 users
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // Less than 1% error rate
},
};
export default function() {
const response = http.get('https://api.example.com/users');
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
import { performance } from 'perf_hooks';
describe('Performance Benchmarks', () => {
test('database query completes in under 100ms', async () => {
const start = performance.now();
await database.query('SELECT * FROM users WHERE email = ?', ['test@example.com']);
const duration = performance.now() - start;
expect(duration).toBeLessThan(100);
});
test('API endpoint responds in under 200ms', async () => {
const start = performance.now();
await request(app).get('/api/users/123');
const duration = performance.now() - start;
expect(duration).toBeLessThan(200);
});
});
1. Race Conditions:
// ❌ BAD - Race condition
test('displays data', async () => {
fetchData();
expect(screen.getByText('Data loaded')).toBeInTheDocument();
// Fails intermittently if fetchData takes longer than expected
});
// ✅ GOOD - Wait for async operation
test('displays data', async () => {
fetchData();
await screen.findByText('Data loaded'); // Waits up to 1 second
});
2. Time Dependencies:
// ❌ BAD - Depends on current time
test('shows message for new users', () => {
const user = { createdAt: new Date() };
expect(isNewUser(user)).toBe(true);
// Fails if test runs slowly
});
// ✅ GOOD - Mock time
test('shows message for new users', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-10-16'));
const user = { createdAt: new Date('2025-10-15') };
expect(isNewUser(user)).toBe(true);
jest.useRealTimers();
});
3. Shared State:
// ❌ BAD - Tests share state
let counter = 0;
test('increments counter', () => {
counter++;
expect(counter).toBe(1);
});
test('increments counter again', () => {
counter++;
expect(counter).toBe(1); // Fails if first test ran
});
// ✅ GOOD - Isolated state
test('increments counter', () => {
const counter = new Counter();
counter.increment();
expect(counter.value).toBe(1);
});
afterEach(async () => {
await database.truncate();
jest.clearAllMocks();
jest.useRealTimers();
});
// ❌ BAD
await sleep(1000);
// ✅ GOOD
await waitFor(() => expect(element).toBeInTheDocument());
test('creates user', async () => {
const uniqueEmail = `test-${Date.now()}@example.com`;
const user = await createUser({ email: uniqueEmail });
expect(user.email).toBe(uniqueEmail);
});
// fixtures/users.ts
export const testUsers = {
admin: {
email: 'admin@example.com',
password: 'AdminPass123',
role: 'admin',
},
regular: {
email: 'user@example.com',
password: 'UserPass123',
role: 'user',
},
};
// Usage in tests
import { testUsers } from './fixtures/users';
test('admin can delete users', async () => {
const admin = await createUser(testUsers.admin);
// Test admin functionality
});
class UserFactory {
static create(overrides = {}) {
return {
id: faker.datatype.uuid(),
email: faker.internet.email(),
name: faker.name.fullName(),
createdAt: new Date(),
...overrides,
};
}
static createMany(count: number, overrides = {}) {
return Array.from({ length: count }, () => this.create(overrides));
}
}
// Usage
test('displays user list', () => {
const users = UserFactory.createMany(5);
render(<UserList users={users} />);
expect(screen.getAllByRole('listitem')).toHaveLength(5);
});
// seeds/test-seed.ts
export async function seedTestDatabase() {
// Create admin user
const admin = await prisma.user.create({
data: { email: 'admin@test.com', role: 'admin' },
});
// Create test products
const products = await Promise.all([
prisma.product.create({ data: { name: 'Product 1', price: 10 } }),
prisma.product.create({ data: { name: 'Product 2', price: 20 } }),
]);
return { admin, products };
}
// Usage
beforeEach(async () => {
await prisma.$executeRaw`TRUNCATE TABLE users CASCADE`;
const { admin, products } = await seedTestDatabase();
});
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run type-check
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
- name: Run E2E tests
run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
fail_ci_if_error: true
// package.json
{
"scripts": {
"test": "npm run test:unit && npm run test:integration && npm run test:e2e",
"test:unit": "jest --testPathPattern=\\.test\\.ts$",
"test:integration": "jest --testPathPattern=\\.integration\\.ts$",
"test:e2e": "playwright test",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --maxWorkers=2"
}
}
Parallel execution:
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npm test -- --shard=${{ matrix.shard }}/4
Cache dependencies:
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
tests/
├── unit/ # Fast, isolated tests
│ ├── services/
│ │ ├── user-service.test.ts
│ │ └── order-service.test.ts
│ └── utils/
│ ├── validator.test.ts
│ └── formatter.test.ts
├── integration/ # Database, API tests
│ ├── api/
│ │ ├── users.integration.ts
│ │ └── orders.integration.ts
│ └── database/
│ └── repository.integration.ts
├── e2e/ # End-to-end tests
│ ├── auth.spec.ts
│ ├── checkout.spec.ts
│ └── profile.spec.ts
├── fixtures/ # Test data
│ ├── users.ts
│ └── products.ts
└── helpers/ # Test utilities
├── setup.ts
└── factories.ts
// Pattern: describe('Component/Function', () => test('should...when...'))
describe('UserService', () => {
describe('register', () => {
test('should create user when valid data provided', async () => {
// Test implementation
});
test('should throw error when email already exists', async () => {
// Test implementation
});
test('should hash password before saving', async () => {
// Test implementation
});
});
describe('login', () => {
test('should return token when credentials are valid', async () => {
// Test implementation
});
test('should throw error when password is incorrect', async () => {
// Test implementation
});
});
});
Use this skill when:
Remember: Good tests give you confidence to refactor, catch bugs early, and serve as living documentation. Invest in your test suite and it will pay dividends throughout the project lifecycle.
npx skills add webdevtodayjason/testing-strategy下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer