| name | e2e-testing-backend |
| description | End-to-end testing patterns for backend services. Use when testing complete application flows. |
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
import { execSync, spawn, ChildProcess } from 'child_process';
let serverProcess: ChildProcess | null = null;
export async function startServer(): Promise<void> {
execSync('npm run build', { stdio: 'inherit' });
serverProcess = spawn('node', ['dist/index.js'], {
env: {
...process.env,
NODE_ENV: 'test',
PORT: '3001',
},
stdio: 'pipe',
});
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 {
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(`Server did not start within ${timeout}ms`);
}
Vitest Configuration
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
import { execSync } from 'child_process';
export async function setup(): Promise<void> {
console.log('Setting up E2E environment...');
execSync('docker-compose -f docker-compose.test.yml up -d', {
stdio: 'inherit',
});
await waitForPostgres();
await waitForRedis();
execSync('npx prisma migrate deploy', { stdio: 'inherit' });
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', {
: ,
});
}
(): <> {
maxAttempts = ;
( i = ; i < maxAttempts; i++) {
{
(, {
: ,
});
;
} {
( (resolve, ));
}
}
();
}
(): <> {
maxAttempts = ;
( i = ; i < maxAttempts; i++) {
{
(, {
: ,
});
;
} {
( (resolve, ));
}
}
();
}
Docker Compose for Tests
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
Complete Flow Test
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 = response.();
(data..).(testUser.);
(data.).();
(data.).();
accessToken = data.;
refreshToken = data.;
userId = data..;
});
(, () => {
response = (, {
: ,
: { : },
: .({
: testUser.,
: testUser.,
}),
});
(response.).();
data = response.();
(data.).();
accessToken = data.;
});
(, () => {
response = (, {
: { : },
});
(response.).();
data = response.();
(data.).(testUser.);
(data.).(testUser.);
});
(, () => {
response = (, {
: ,
: { : },
: .({ refreshToken }),
});
(response.).();
data = response.();
(data.).();
(data.)..(accessToken);
});
(, () => {
response = (, {
: ,
: { : },
});
(response.).();
});
(, () => {
response = (, {
: ,
: { : },
: .({ refreshToken }),
});
(response.).();
});
});
CRUD Flow Test
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 () => {
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`, {
: ,
: {
: ,
: ,
},
: .({
: ,
: ,
: ,
}),
});
(response.).();
data = response.();
(data.).();
postId = data.;
});
(, () => {
response = (, {
: { : },
});
(response.).();
data = response.();
(data.).();
(data.).();
});
(, () => {
response = (, {
: ,
: {
: ,
: ,
},
: .({
: ,
: ,
}),
});
(response.).();
data = response.();
(data.).();
(data.).();
});
(, () => {
response = (, {
: { : },
});
(response.).();
data = response.();
post = data..( p. === postId);
(post).();
(post.).();
});
(, () => {
response = (, {
: ,
: { : },
});
(response.).();
});
(, () => {
response = (, {
: { : },
});
(response.).();
});
});
API Client Helper
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) : ,
});
}
(): <{ : ; : }> {
response = (, {
: ,
: { email, password },
});
(!response.) {
();
}
response.();
}
Running E2E Tests
npm run test:e2e
API_URL=http://localhost:3000 npm run test:e2e
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