Automated Testing Pyramid: Unit, Integration, and E2E Done Right
Most teams have the testing pyramid inverted. Too many slow E2E tests, not enough fast unit tests. Here's the right balance.
The Pyramid
/\
/E2E\ ← Few, slow, test user journeys
/------\
/Integr. \ ← Some, test service boundaries
/----------\
/ Unit \ ← Many, fast, test business logic
/--------------\
Ratio: 70% unit / 20% integration / 10% E2E
Unit Tests: Fast, Isolated
// Test pure business logic — no DB, no network
import { describe, it, expect } from 'vitest';
import { calculateDiscount } from './pricing';
describe('calculateDiscount', () => {
it('applies 10% for orders over $100', () => {
expect(calculateDiscount(150_00)).toBe(15_00); // cents
});
it('no discount for orders under $100', () => {
expect(calculateDiscount(50_00)).toBe(0);
});
it('caps discount at $50', () => {
expect(calculateDiscount(1000_00)).toBe(50_00);
});
});
Integration Tests: Real Database
// Test against a real test database
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
import { PrismaClient } from '@prisma/client';
import { UserService } from './UserService';
const db = new PrismaClient();
beforeEach(async () => {
await db.user.deleteMany(); // Clean slate
});
afterAll(() => db.$disconnect());
describe('UserService', () => {
it('creates a user in the database', async () => {
const service = new UserService(db);
const user = await service.create({ email: 'test@example.com', name: 'Test' });
const stored = await db.user.findUnique({ where: { id: user.id } });
expect(stored?.email).toBe('test@example.com');
});
});
E2E Tests: Critical User Journeys Only
// Playwright — only test the paths that matter most
import { test, expect } from '@playwright/test';
test('user can sign up and complete onboarding', async ({ page }) => {
await page.goto('/signup');
await page.fill('[name=email]', 'test@example.com');
await page.fill('[name=password]', 'securepass123');
await page.click('button[type=submit]');
await expect(page).toHaveURL('/onboarding');
await expect(page.locator('h1')).toContainText('Welcome');
});
test('user can complete a purchase', async ({ page }) => {
// Test the full checkout flow with Stripe test cards
await page.goto('/pricing');
await page.click('text=Get Started');
// ... fill Stripe checkout ...
await expect(page).toHaveURL('/dashboard');
});
CI Pipeline
jobs:
test:
steps:
- run: npm run test:unit # Fast, runs always
- run: npm run test:integration # Medium, needs test DB
- run: npm run test:e2e # Slow, runs on main branch only
Vitest config, Playwright setup, test database patterns, and a complete testing pyramid are pre-configured in the AI SaaS Starter Kit.
Top comments (0)