DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Automated Testing Pyramid: Unit, Integration, and E2E Done Right

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
  /--------------\
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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');
  });
});
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Vitest config, Playwright setup, test database patterns, and a complete testing pyramid are pre-configured in the AI SaaS Starter Kit.

Top comments (0)