Comprehensive testing patterns and anti-patterns for writing and reviewing tests
Use this skill when writing tests, reviewing test code, or investigating test failures.
Read the relevant reference based on context:
| Context | Reference |
|---|---|
| General testing | This document |
| Anti-patterns | Anti-Patterns section below |
| Patterns | Patterns section below |
| Server testing | Server Testing section below |
| Evolution testing | section below |
| Smoke testing | Smoke Testing section below |
This project uses standalone test scripts — no test framework (no Jest, Mocha, etc.):
test-evolution-loop.js # Integration test: signal → insight → lesson pipeline
smoke-test.js # HTTP endpoint validation (port-based, reusable)
Convention: Tests are standalone Node.js scripts that start the server, exercise endpoints, and validate results. Run with node <test-file>.js.
# Run main integration test
npm test # → node test-evolution-loop.js
# Run smoke test against running server
node smoke-test.js 3461 # Validate all HTTP endpoints
# Run smoke test for specific endpoint
node smoke-test.js 3461 /api/controls
# Run syntax checks on all source files
node --check server.js && node --check management.js && node --check retro.js
Tests typically follow this pattern:
const http = require('http');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
// 1. Start server as child process
const server = spawn('node', ['server.js'], {
env: { ...process.env, PORT: '0' }, // random port
stdio: ['pipe', 'pipe', 'pipe'],
});
// 2. Wait for server to be ready (parse port from stdout)
// 3. Run test requests
// 4. Validate responses and board state
// 5. Kill server and clean up
process.on('exit', () => {
server.kill();
// Clean up test board.json if created
});
Key points:
process.on('exit') for cleanup guaranteefunction request(port, method, path, body = null) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'localhost',
port,
path,
method,
headers: body ? { 'Content-Type': 'application/json' } : {},
};
const req = http.request(options, res => {
let data = '';
res.on('data', chunk => { data += chunk; });
res.on('end', () => {
resolve({ status: res.statusCode, body: data, json: () => JSON.parse(data) });
});
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
// Usage
const res = await request(port, 'GET', '/api/board');
assert(res.status === 200);
const board = res.json();
assert(board.taskPlan !== undefined);
// Instead of mocking internals, check board.json state
async function test_taskCreation(port) {
// Act: create task plan via API
await request(port, 'POST', '/api/tasks', {
goal: 'Test goal',
phase: 'test',
tasks: [{ id: 'T1', title: 'test task', spec: 'do something', status: 'pending' }],
});
// Assert: verify board state via API
const res = await request(port, 'GET', '/api/tasks');
const plan = res.json();
assert(plan.tasks.length === 1);
assert(plan.tasks[0].id === 'T1');
assert(plan.tasks[0].status === 'pending');
}
async function test_taskLifecycle(port) {
// Setup: create task plan
await request(port, 'POST', '/api/tasks', {
goal: 'lifecycle test',
phase: 'test',
tasks: [{ id: 'T1', title: 'task', spec: 'spec', status: 'pending' }],
});
// Dispatch
await request(port, 'POST', '/api/tasks/T1/dispatch', { assignee: 'agent-1' });
let board = (await request(port, 'GET', '/api/board')).json();
assert(board.taskPlan.tasks[0].status === 'dispatched');
// Update to in_progress
await request(port, 'POST', '/api/tasks/T1/status', { status: 'in_progress' });
board = (await request(port, 'GET', '/api/board')).json();
assert(board.taskPlan.tasks[0].status === 'in_progress');
// Complete
await request(port, 'POST', '/api/tasks/T1/update', {
status: 'completed',
result: 'Task completed successfully',
});
board = (await request(port, 'GET', '/api/board')).json();
assert(board.taskPlan.tasks[0].status === 'completed');
}
async function test_sseEvents(port) {
return new Promise((resolve, reject) => {
http.get(`http://localhost:${port}/api/events`, res => {
let received = '';
res.on('data', chunk => {
received += chunk.toString();
if (received.includes('data:')) {
res.destroy(); // Got an event, test passes
resolve();
}
});
setTimeout(() => {
res.destroy();
reject(new Error('No SSE event received within timeout'));
}, 5000);
});
// Trigger an event by modifying the board
setTimeout(() => {
request(port, 'POST', '/api/tasks', { goal: 'sse test', phase: 'test', tasks: [] });
}, 100);
});
}
// BAD: Mocking internal board loading
const originalLoadBoard = loadBoard;
loadBoard = () => ({ taskPlan: { tasks: [] } }); // mocked
// test...
loadBoard = originalLoadBoard;
// GOOD: Test through HTTP API with real board
await request(port, 'POST', '/api/tasks', { goal: 'test', phase: 'p', tasks: [] });
const board = (await request(port, 'GET', '/api/board')).json();
assert(board.taskPlan.tasks.length === 0);
// BAD: Asserting internal file format
const raw = fs.readFileSync('board.json', 'utf8');
assert(raw.includes('"status":"pending"'));
// GOOD: Test through public API behavior
const res = await request(port, 'GET', '/api/tasks');
const plan = res.json();
assert(plan.tasks[0].status === 'pending');
// BAD: Tests depend on shared board.json
// Test A creates tasks, Test B expects them to exist
// GOOD: Each test sets up its own state
async function test_something(port) {
// Setup: create fresh task plan
await request(port, 'POST', '/api/tasks', {
goal: 'isolated test',
phase: 'test',
tasks: [{ id: 'T1', title: 'test', spec: 'spec', status: 'pending' }],
});
// Test against this specific state
}
// BAD: Only checking HTTP status
const res = await request(port, 'GET', '/api/board');
assert(res.status === 200); // Passes even if response body is wrong!
// GOOD: Check status AND body
const res = await request(port, 'GET', '/api/board');
assert(res.status === 200);
const board = res.json();
assert(board.taskPlan !== undefined);
assert(Array.isArray(board.taskPlan.tasks));
// BAD: Arbitrary sleep hoping server is ready
await new Promise(r => setTimeout(r, 5000));
// GOOD: Poll until server responds
async function waitForServer(port, maxWait = 10000) {
const start = Date.now();
while (Date.now() - start < maxWait) {
try {
await request(port, 'GET', '/api/board');
return; // Server is ready
} catch {
await new Promise(r => setTimeout(r, 100));
}
}
throw new Error('Server did not start within timeout');
}
// BAD: Leaving server process running
const server = spawn('node', ['server.js']);
// tests run...
// server still running after tests finish!
// GOOD: Always clean up
const server = spawn('node', ['server.js']);
try {
await waitForServer(port);
await runTests(port);
} finally {
server.kill();
// Clean up test artifacts
try { fs.unlinkSync('test-board.json'); } catch {}
}
Tests for server.js HTTP endpoints.
Key patterns:
node server.js as child processEndpoints to test:
GET /api/board — returns full boardPOST /api/tasks — creates task planPOST /api/tasks/:id/dispatch — dispatches taskPOST /api/tasks/:id/status — updates statusPOST /api/tasks/:id/update — updates task fieldsPOST /api/tasks/:id/unblock — unblocks taskPOST /api/tasks/dispatch — batch dispatchGET /api/signals — returns signalsPOST /api/signals — emits signalGET /api/insights — returns insightsGET /api/events — SSE streamTests for the signal → insight → lesson pipeline (test-evolution-loop.js).
Key patterns:
/api/signals/api/insightsQuick validation that all endpoints respond correctly (smoke-test.js).
Key patterns:
node smoke-test.js 3461 /api/controlsBefore submitting:
node <test-file>.js.claude/skills/code-quality/SKILL.md.claude/skills/project-principles/SKILL.md