Testing Strategy Guide
A comprehensive approach to testing software at every level of the testing pyramid.
Purpose
Good tests give you confidence to ship fast. Bad tests slow you down and give false confidence. This skill helps you write the right tests at the right level, maximizing value while minimizing maintenance burden.
Prerequisites
Steps
Step 1: Identify What to Test
Use the testing pyramid as a guide:
/\
/ \ E2E (few, slow, high confidence)
/----\
/ \ Integration (some, medium speed)
/--------\
/ \ Unit (many, fast, isolated)
--------------
Test Priority:
1. Critical user paths (auth, checkout, data operations)
2. Complex business logic
3. Edge cases and error handling
4. Recently fixed bugs (regression tests)
Expected outcome: Clear list of what needs testing.
Step 2: Write Unit Tests
Test individual functions in isolation.
// Example: Testing a utility function
describe('calculateDiscount', () => {
it('applies percentage discount correctly', () => {
expect(calculateDiscount(100, 10)).toBe(90);
}); it('handles zero discount', () => {
expect(calculateDiscount(100, 0)).toBe(100);
});
it('throws on negative discount', () => {
expect(() => calculateDiscount(100, -10)).toThrow();
});
it('handles decimal precision', () => {
expect(calculateDiscount(100, 33.33)).toBeCloseTo(66.67);
});
});
Expected outcome: Fast, isolated tests for logic.
Step 3: Write Integration Tests
Test components working together.
// Example: Testing API route with database
describe('POST /api/users', () => {
beforeEach(async () => {
await db.reset();
}); it('creates user and returns 201', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', name: 'Test' });
expect(response.status).toBe(201);
expect(response.body.id).toBeDefined();
// Verify persistence
const user = await db.users.findById(response.body.id);
expect(user.email).toBe('test@example.com');
});
it('returns 400 on invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'invalid', name: 'Test' });
expect(response.status).toBe(400);
});
});
Expected outcome: Confidence that parts work together.
Step 4: Write E2E Tests (Sparingly)
Test critical user flows end-to-end.
// Example: Playwright E2E test
test('user can sign up and make purchase', async ({ page }) => {
// Sign up
await page.goto('/signup');
await page.fill('[name="email"]', 'new@example.com');
await page.fill('[name="password"]', 'SecurePass123!');
await page.click('button[type="submit"]');
await expect(page.locator('text=Welcome')).toBeVisible();
// Make purchase
await page.goto('/products');
await page.click('text=Add to Cart');
await page.click('text=Checkout');
await page.fill('[name="card"]', '4242424242424242');
await page.click('text=Pay');
await expect(page.locator('text=Order confirmed')).toBeVisible();
});
Expected outcome: Critical paths verified end-to-end.
Checks
Automated Checks
Run all tests
npm testRun with coverage
npm test -- --coverageVerify coverage thresholds
npm test -- --coverage --coverageThreshold='{"global":{"branches":80}}'
Manual Checks
Failure Modes
Rollback
If tests become problematic:
Skip flaky test temporarily (fix soon!)
it.skip('flaky test', () => {});Mark test as todo
it.todo('test to implement later');
Variations
TDD Flow
1. Write failing test
2. Write minimum code to pass
3. Refactor
4. Repeat
Testing Legacy Code
1. Write characterization tests (capture current behavior)
2. Refactor with safety net
3. Replace characterization tests with proper unit tests
