Claude-skill-registry e2e-testing-backend
End-to-end testing patterns for backend services. Use when testing complete application flows.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/e2e-testing-backend" ~/.claude/skills/majiayu000-claude-skill-registry-e2e-testing-backend && rm -rf "$T"
manifest:
skills/data/e2e-testing-backend/SKILL.mdsource content
E2E Testing Backend Skill
This skill covers end-to-end testing patterns for Node.js backend services.
When to Use
Use this skill when:
- Testing complete user flows
- Verifying multi-service integration
- Testing deployment readiness
- Validating production-like scenarios
Core Principle
TEST LIKE A USER - E2E tests verify the system works as users expect. Test complete flows, not individual parts.
Setup
// tests/e2e/setup.ts import { execSync, spawn, ChildProcess } from 'child_process'; let serverProcess: ChildProcess | null = null; export async function startServer(): Promise<void> { // Build the application execSync('npm run build', { stdio: 'inherit' }); // Start the server serverProcess = spawn('node', ['dist/index.js'], { env: { ...process.env, NODE_ENV: 'test', PORT: '3001', }, stdio: 'pipe', }); // Wait for server to be ready await waitForServer('http://localhost:3001/health', 30000); } export async function stopServer(): Promise<void> { if (serverProcess) { serverProcess.kill(); serverProcess = null; } } async function waitForServer(url: string, timeout: number): Promise<void> { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { const response = await fetch(url); if (response.ok) return; } catch { // Server not ready yet } await new Promise((resolve) => setTimeout(resolve, 500)); } throw new Error(`Server did not start within ${timeout}ms`); }
Vitest Configuration
// vitest.e2e.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { include: ['tests/e2e/**/*.e2e.test.ts'], testTimeout: 60000, hookTimeout: 30000, globalSetup: './tests/e2e/global-setup.ts', setupFiles: ['./tests/e2e/setup-file.ts'], pool: 'forks', poolOptions: { forks: { singleFork: true, }, }, }, });
Global Setup
// tests/e2e/global-setup.ts import { execSync } from 'child_process'; export async function setup(): Promise<void> { console.log('Setting up E2E environment...'); // Start required services execSync('docker-compose -f docker-compose.test.yml up -d', { stdio: 'inherit', }); // Wait for services to be ready await waitForPostgres(); await waitForRedis(); // Run migrations execSync('npx prisma migrate deploy', { stdio: 'inherit' }); // Seed test data execSync('npx prisma db seed', { stdio: 'inherit' }); console.log('E2E environment ready'); } export async function teardown(): Promise<void> { console.log('Tearing down E2E environment...'); execSync('docker-compose -f docker-compose.test.yml down', { stdio: 'inherit', }); } async function waitForPostgres(): Promise<void> { const maxAttempts = 30; for (let i = 0; i < maxAttempts; i++) { try { execSync('docker-compose -f docker-compose.test.yml exec -T db pg_isready', { stdio: 'pipe', }); return; } catch { await new Promise((resolve) => setTimeout(resolve, 1000)); } } throw new Error('PostgreSQL did not start'); } async function waitForRedis(): Promise<void> { const maxAttempts = 30; for (let i = 0; i < maxAttempts; i++) { try { execSync('docker-compose -f docker-compose.test.yml exec -T redis redis-cli ping', { stdio: 'pipe', }); return; } catch { await new Promise((resolve) => setTimeout(resolve, 1000)); } } throw new Error('Redis did not start'); }
Docker Compose for Tests
# docker-compose.test.yml version: '3.8' services: db: image: postgres:16-alpine environment: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: testdb ports: - "5433:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U test"] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - "6380:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 5 api: build: . environment: NODE_ENV: test DATABASE_URL: postgresql://test:test@db:5432/testdb REDIS_URL: redis://redis:6379 PORT: 3000 ports: - "3001:3000" depends_on: db: condition: service_healthy redis: condition: service_healthy
Complete Flow Test
// tests/e2e/auth-flow.e2e.test.ts import { describe, it, expect, beforeAll, afterAll } from 'vitest'; const API_URL = process.env.API_URL ?? 'http://localhost:3001'; describe('Authentication Flow E2E', () => { const testUser = { email: `e2e-${Date.now()}@example.com`, password: 'Password123!', name: 'E2E Test User', }; let accessToken: string; let refreshToken: string; let userId: string; it('registers a new user', async () => { const response = await fetch(`${API_URL}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(testUser), }); expect(response.status).toBe(201); const data = await response.json(); expect(data.user.email).toBe(testUser.email); expect(data.accessToken).toBeDefined(); expect(data.refreshToken).toBeDefined(); accessToken = data.accessToken; refreshToken = data.refreshToken; userId = data.user.id; }); it('logs in with registered credentials', async () => { const response = await fetch(`${API_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: testUser.email, password: testUser.password, }), }); expect(response.status).toBe(200); const data = await response.json(); expect(data.accessToken).toBeDefined(); accessToken = data.accessToken; }); it('accesses protected resource with token', async () => { const response = await fetch(`${API_URL}/api/users/me`, { headers: { Authorization: `Bearer ${accessToken}` }, }); expect(response.status).toBe(200); const data = await response.json(); expect(data.email).toBe(testUser.email); expect(data.name).toBe(testUser.name); }); it('refreshes access token', async () => { const response = await fetch(`${API_URL}/api/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }), }); expect(response.status).toBe(200); const data = await response.json(); expect(data.accessToken).toBeDefined(); expect(data.accessToken).not.toBe(accessToken); }); it('logs out successfully', async () => { const response = await fetch(`${API_URL}/api/auth/logout`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, }); expect(response.status).toBe(200); }); it('rejects requests after logout', async () => { const response = await fetch(`${API_URL}/api/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }), }); expect(response.status).toBe(401); }); });
CRUD Flow Test
// tests/e2e/posts-crud.e2e.test.ts import { describe, it, expect, beforeAll } from 'vitest'; const API_URL = process.env.API_URL ?? 'http://localhost:3001'; describe('Posts CRUD Flow E2E', () => { let authToken: string; let postId: string; beforeAll(async () => { // Login to get auth token const response = await fetch(`${API_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'e2e-user@example.com', password: 'Password123!', }), }); const data = await response.json(); authToken = data.accessToken; }); it('creates a post', async () => { const response = await fetch(`${API_URL}/api/posts`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ title: 'E2E Test Post', content: 'This is an E2E test post', published: false, }), }); expect(response.status).toBe(201); const data = await response.json(); expect(data.title).toBe('E2E Test Post'); postId = data.id; }); it('retrieves the created post', async () => { const response = await fetch(`${API_URL}/api/posts/${postId}`, { headers: { Authorization: `Bearer ${authToken}` }, }); expect(response.status).toBe(200); const data = await response.json(); expect(data.title).toBe('E2E Test Post'); expect(data.published).toBe(false); }); it('updates the post', async () => { const response = await fetch(`${API_URL}/api/posts/${postId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ title: 'Updated E2E Test Post', published: true, }), }); expect(response.status).toBe(200); const data = await response.json(); expect(data.title).toBe('Updated E2E Test Post'); expect(data.published).toBe(true); }); it('lists posts including the new one', async () => { const response = await fetch(`${API_URL}/api/posts?published=true`, { headers: { Authorization: `Bearer ${authToken}` }, }); expect(response.status).toBe(200); const data = await response.json(); const post = data.data.find((p: { id: string }) => p.id === postId); expect(post).toBeDefined(); expect(post.title).toBe('Updated E2E Test Post'); }); it('deletes the post', async () => { const response = await fetch(`${API_URL}/api/posts/${postId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${authToken}` }, }); expect(response.status).toBe(204); }); it('returns 404 for deleted post', async () => { const response = await fetch(`${API_URL}/api/posts/${postId}`, { headers: { Authorization: `Bearer ${authToken}` }, }); expect(response.status).toBe(404); }); });
API Client Helper
// tests/e2e/helpers/api-client.ts const API_URL = process.env.API_URL ?? 'http://localhost:3001'; interface RequestOptions { method?: string; body?: unknown; headers?: Record<string, string>; token?: string; } export async function apiRequest( path: string, options: RequestOptions = {} ): Promise<Response> { const { method = 'GET', body, headers = {}, token } = options; const requestHeaders: Record<string, string> = { 'Content-Type': 'application/json', ...headers, }; if (token) { requestHeaders['Authorization'] = `Bearer ${token}`; } return fetch(`${API_URL}${path}`, { method, headers: requestHeaders, body: body ? JSON.stringify(body) : undefined, }); } export async function login( email: string, password: string ): Promise<{ accessToken: string; refreshToken: string }> { const response = await apiRequest('/api/auth/login', { method: 'POST', body: { email, password }, }); if (!response.ok) { throw new Error(`Login failed: ${response.status}`); } return response.json(); }
Running E2E Tests
# Start services and run tests npm run test:e2e # Run against existing services API_URL=http://localhost:3000 npm run test:e2e # Run specific test file npm run test:e2e -- auth-flow.e2e.test.ts
Package.json Scripts
{ "scripts": { "test:e2e": "docker-compose -f docker-compose.test.yml up -d && vitest run --config vitest.e2e.config.ts; docker-compose -f docker-compose.test.yml down", "test:e2e:watch": "docker-compose -f docker-compose.test.yml up -d && vitest --config vitest.e2e.config.ts" } }
Best Practices
- Test complete flows - Registration to logout
- Isolate test data - Use unique identifiers
- Clean up after tests - Delete created resources
- Use real services - No mocking in E2E
- Test error scenarios - Invalid data, auth failures
- Parallel-safe - Tests should not interfere
Notes
- E2E tests are slowest - run sparingly
- Use in CI/CD before deployment
- Test against staging environment
- Monitor test flakiness