Testing
DevOps
Testing Strategies
for Serverless
Unit tests, integration tests, mocking Workers, and testing patterns. Ship with confidence.
Testing serverless is different. No long-running servers to hit with requests. Bindings like KV and D1 that need mocking. Edge runtime quirks that don't exist locally.
Here's how we test 28 Workers with confidence.
Testing Pyramid
E2E Tests
10%
Integration Tests
20%
Unit Tests
70%
Pattern 1: Unit Tests with Vitest
lead-scoring.test.ts
import { describe, it, expect } from 'vitest';
import { calculateLeadScore, parseIntent } from './lead-scoring';
describe('Lead Scoring', () => {
describe('calculateLeadScore', () => {
it('scores urgent seller high', () => {
const lead = {
message: 'Need to sell ASAP, relocating next month',
source: 'google_ad',
hasPhone: true
};
const score = calculateLeadScore(lead);
expect(score.value).toBeGreaterThanOrEqual(80);
expect(score.urgency).toBe('hot');
});
it('scores browsing user low', () => {
const lead = {
message: 'Just curious about home values',
source: 'organic',
hasPhone: false
};
const score = calculateLeadScore(lead);
expect(score.value).toBeLessThanOrEqual(40);
expect(score.urgency).toBe('cold');
});
});
describe('parseIntent', () => {
it('detects sell intent', () => {
expect(parseIntent('I want to sell my house')).toBe('sell');
expect(parseIntent('Looking to list my property')).toBe('sell');
});
it('detects buy intent', () => {
expect(parseIntent('Looking to buy a home')).toBe('buy');
expect(parseIntent('Want to purchase property')).toBe('buy');
});
});
});
Pattern 2: Integration Tests with Miniflare
api.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { unstable_dev } from 'wrangler';
import type { UnstableDevWorker } from 'wrangler';
describe('API Integration', () => {
let worker: UnstableDevWorker;
beforeAll(async () => {
worker = await unstable_dev('src/index.ts', {
experimental: { disableExperimentalWarning: true }
});
});
afterAll(async () => {
await worker.stop();
});
it('creates a lead', async () => {
const response = await worker.fetch('/api/leads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Test User',
email: '[email protected]',
message: 'Test message'
})
});
expect(response.status).toBe(201);
const data = await response.json();
expect(data.id).toBeDefined();
expect(data.name).toBe('Test User');
});
it('returns 400 for invalid email', async () => {
const response = await worker.fetch('/api/leads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Test',
email: 'invalid-email'
})
});
expect(response.status).toBe(400);
const error = await response.json();
expect(error.error.code).toBe('VALIDATION_ERROR');
expect(error.error.field).toBe('email');
});
});
Pattern 3: Mocking Bindings
mocks.ts
// Mock KV for unit tests
export function createMockKV(): KVNamespace {
const store = new Map<string, string>();
return {
async get(key: string, options?: any) {
const value = store.get(key);
if (!value) return null;
if (options === 'json') return JSON.parse(value);
return value;
},
async put(key: string, value: string) {
store.set(key, value);
},
async delete(key: string) {
store.delete(key);
},
async list() {
return { keys: [...store.keys()].map(name => ({ name })) };
}
} as KVNamespace;
}
// Mock D1 for unit tests
export function createMockD1(): D1Database {
const tables = new Map<string, any[]>();
return {
prepare(query: string) {
return {
bind(...params: any[]) {
return {
async first() {
// Simple mock implementation
return null;
},
async all() {
return { results: [] };
},
async run() {
return { success: true };
}
};
}
};
}
} as D1Database;
}
Pattern 4: Test Setup
vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'miniflare',
environmentOptions: {
modules: true,
kvNamespaces: ['KV'],
d1Databases: ['DB'],
durableObjects: {
SESSION: 'SessionDO'
}
},
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['**/*.test.ts', '**/mocks/**']
},
testTimeout: 10000,
hookTimeout: 10000
}
});
Testing Strategy
Unit test your business logic (scoring, parsing, validation). Integration test your API endpoints with Miniflare. E2E test critical user paths against staging. Skip E2E tests for internal APIs.
Testing Checklist
- Keep business logic in pure functionsโeasy to unit test
- Use Miniflare for integration tests with real bindings
- Mock external APIs to avoid flaky tests
- Test error paths, not just happy paths
- Run tests in CI before every deploy
- Aim for 70% unit, 20% integration, 10% E2E
- Use coverage reports to find gaps
- Test against staging for E2E, not production
Good tests make refactoring safe. If you're afraid to change code because tests might break, your tests are testing implementation, not behavior. Fix that first.