Testing DevOps

Testing Strategies
for Serverless

Unit tests, integration tests, mocking Workers, and testing patterns. Ship with confidence.

๐Ÿ“– 13 min read January 24, 2026

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.

Related Articles

CI/CD for Workers
Read more โ†’
Monitoring & Observability
Read more โ†’
Error Handling Patterns
Read more โ†’

Need Testing Infrastructure?

We build systems that ship with confidence.

Start Project