Testing Strategies: Unit, Integration, E2E, and Contract Testing Explained

Build a comprehensive testing strategy. Unit tests with Vitest, integration tests with Testcontainers, E2E with Playwright, and contract testing with...

Y
Yash Pritwani
15 min read

The Testing Pyramid in 2025

The testing pyramid is still the best mental model for test distribution:

Terminal$docker compose up -d[+] Running 5/5Network app_default CreatedContainer web StartedContainer api StartedContainer db Started$

Docker Compose brings up your entire stack with a single command.

         /\
        /  \       E2E Tests (few, slow, expensive)
       /    \
      /------\
     /        \    Integration Tests (moderate)
    /          \
   /------------\
  /              \  Unit Tests (many, fast, cheap)
 /________________\
  • Unit tests: 70% of your tests. Fast, isolated, test one thing.
  • Integration tests: 20% of your tests. Test component interactions.
  • E2E tests: 10% of your tests. Test full user workflows.

Unit Testing with Vitest

Vitest is the modern standard for JavaScript/TypeScript unit testing. It is compatible with Jest's API but significantly faster (native ESM, Vite-powered).

Get more insights on Tutorials

Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.

// price-calculator.ts
export function calculatePrice(
  basePrice: number,
  quantity: number,
  discount: number,
  taxRate: number
): { subtotal: number; tax: number; total: number } {
  if (quantity < 1) throw new Error('Quantity must be at least 1');
  if (discount < 0 || discount > 100) throw new Error('Discount must be 0-100');

  const subtotal = basePrice * quantity * (1 - discount / 100);
  const tax = subtotal * (taxRate / 100);
  const total = subtotal + tax;

  return {
    subtotal: Math.round(subtotal * 100) / 100,
    tax: Math.round(tax * 100) / 100,
    total: Math.round(total * 100) / 100,
  };
}
// price-calculator.test.ts
import { describe, it, expect } from 'vitest';
import { calculatePrice } from './price-calculator';

describe('calculatePrice', () => {
  it('calculates basic price without discount', () => {
    const result = calculatePrice(100, 2, 0, 18);
    expect(result).toEqual({
      subtotal: 200,
      tax: 36,
      total: 236,
    });
  });

  it('applies percentage discount correctly', () => {
    const result = calculatePrice(100, 1, 20, 18);
    expect(result.subtotal).toBe(80);
    expect(result.total).toBe(94.4);
  });

  it('handles fractional cents with rounding', () => {
    const result = calculatePrice(10.99, 3, 15, 18);
    expect(result.subtotal).toBe(28.02);
    expect(result.tax).toBe(5.04);
    expect(result.total).toBe(33.06);
  });

  it('throws on invalid quantity', () => {
    expect(() => calculatePrice(100, 0, 0, 18)).toThrow('Quantity must be at least 1');
    expect(() => calculatePrice(100, -1, 0, 18)).toThrow('Quantity must be at least 1');
  });

  it('throws on invalid discount', () => {
    expect(() => calculatePrice(100, 1, -5, 18)).toThrow('Discount must be 0-100');
    expect(() => calculatePrice(100, 1, 101, 18)).toThrow('Discount must be 0-100');
  });
});

Mocking dependencies:

// user-service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from './user-service';

// Mock the database module
vi.mock('./database', () => ({
  db: {
    users: {
      findById: vi.fn(),
      create: vi.fn(),
    },
  },
}));

// Mock the email module
vi.mock('./email-service', () => ({
  sendEmail: vi.fn().mockResolvedValue(true),
}));

import { db } from './database';
import { sendEmail } from './email-service';

describe('UserService', () => {
  const service = new UserService();

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('creates user and sends welcome email', async () => {
    vi.mocked(db.users.create).mockResolvedValue({
      id: '1', name: 'Jane', email: '[email protected]',
    });

    const user = await service.createUser({ name: 'Jane', email: '[email protected]' });

    expect(db.users.create).toHaveBeenCalledWith({
      name: 'Jane', email: '[email protected]',
    });
    expect(sendEmail).toHaveBeenCalledWith('[email protected]', 'welcome', expect.any(Object));
    expect(user.id).toBe('1');
  });
});

Integration Testing with Testcontainers

Testcontainers spins up real Docker containers for your tests — real PostgreSQL, real Redis, real RabbitMQ. No mocking, no in-memory fakes.

// user-repository.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { Pool } from 'pg';
import { UserRepository } from './user-repository';
import { runMigrations } from './migrations';

describe('UserRepository (integration)', () => {
  let container: StartedPostgreSqlContainer;
  let pool: Pool;
  let repo: UserRepository;

  beforeAll(async () => {
    // Start a real PostgreSQL container
    container = await new PostgreSqlContainer('postgres:16')
      .withDatabase('test')
      .start();

    pool = new Pool({
      connectionString: container.getConnectionUri(),
    });

    // Run real migrations
    await runMigrations(pool);

    repo = new UserRepository(pool);
  }, 60000); // 60s timeout for container startup

  afterAll(async () => {
    await pool.end();
    await container.stop();
  });

  it('creates and retrieves a user', async () => {
    const created = await repo.create({
      name: 'Jane Doe',
      email: '[email protected]',
    });

    expect(created.id).toBeDefined();
    expect(created.name).toBe('Jane Doe');

    const found = await repo.findById(created.id);
    expect(found).toEqual(created);
  });

  it('enforces unique email constraint', async () => {
    await repo.create({ name: 'User 1', email: '[email protected]' });

    await expect(
      repo.create({ name: 'User 2', email: '[email protected]' })
    ).rejects.toThrow(/unique/i);
  });

  it('handles pagination correctly', async () => {
    // Insert 25 users
    for (let i = 0; i < 25; i++) {
      await repo.create({ name: 'User ' + i, email: 'user' + i + '@test.com' });
    }

    const page1 = await repo.findAll({ page: 1, limit: 10 });
    const page2 = await repo.findAll({ page: 2, limit: 10 });
    const page3 = await repo.findAll({ page: 3, limit: 10 });

    expect(page1.length).toBe(10);
    expect(page2.length).toBe(10);
    expect(page3.length).toBeGreaterThanOrEqual(5);
  });
});
InputHiddenHiddenOutput

Neural network architecture: data flows through input, hidden, and output layers.

E2E Testing with Playwright

Playwright tests your application from the user's perspective in a real browser.

// tests/e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Checkout Flow', () => {
  test.beforeEach(async ({ page }) => {
    // Login before each test
    await page.goto('/login');
    await page.fill('[name="email"]', '[email protected]');
    await page.fill('[name="password"]', 'testpass');
    await page.click('button[type="submit"]');
    await page.waitForURL('/dashboard');
  });

  test('complete purchase flow', async ({ page }) => {
    // Browse products
    await page.goto('/products');
    await expect(page.getByText('Product Catalog')).toBeVisible();

    // Add item to cart
    await page.click('[data-testid="product-widget-1"] >> text=Add to Cart');
    await expect(page.getByTestId('cart-count')).toHaveText('1');

    // Go to cart
    await page.click('[data-testid="cart-icon"]');
    await expect(page.getByText('Shopping Cart')).toBeVisible();
    await expect(page.getByText('Widget Pro')).toBeVisible();

    // Proceed to checkout
    await page.click('text=Proceed to Checkout');

    // Fill shipping info
    await page.fill('[name="address"]', '123 Main St');
    await page.fill('[name="city"]', 'Mumbai');
    await page.fill('[name="zip"]', '400001');

    // Submit order
    await page.click('text=Place Order');

    // Verify confirmation
    await expect(page.getByText('Order Confirmed')).toBeVisible();
    await expect(page.getByTestId('order-id')).toBeVisible();
  });

  test('shows error for out-of-stock items', async ({ page }) => {
    await page.goto('/products');
    const outOfStockItem = page.getByTestId('product-out-of-stock');
    await expect(outOfStockItem.getByText('Out of Stock')).toBeVisible();
    await expect(outOfStockItem.getByText('Add to Cart')).toBeDisabled();
  });
});

Contract Testing with Pact

Contract testing ensures that services agree on API contracts without end-to-end tests.

Free Resource

Free Cloud Architecture Checklist

A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.

Download the Checklist
// Consumer side (frontend/mobile app)
import { PactV3, MatchersV3 } from '@pact-foundation/pact';

const provider = new PactV3({
  consumer: 'WebApp',
  provider: 'UserAPI',
});

describe('User API Contract', () => {
  it('returns user by ID', async () => {
    await provider
      .given('user 123 exists')
      .uponReceiving('a request for user 123')
      .withRequest({
        method: 'GET',
        path: '/api/users/123',
        headers: { Accept: 'application/json' },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: MatchersV3.string('123'),
          name: MatchersV3.string('Jane Doe'),
          email: MatchersV3.email(),
          createdAt: MatchersV3.iso8601DateTime(),
        },
      })
      .executeTest(async (mockServer) => {
        const response = await fetch(mockServer.url + '/api/users/123', {
          headers: { Accept: 'application/json' },
        });
        const user = await response.json();

        expect(user.id).toBe('123');
        expect(user.name).toBeDefined();
        expect(user.email).toContain('@');
      });
  });
});

Test Strategy by Layer

Layer Tool What it tests Speed Confidence
Unit Vitest/Jest Business logic, pure functions Very fast (ms) Low (isolated)
Integration Testcontainers Database queries, API handlers Moderate (seconds) Medium
Contract Pact API compatibility between services Fast (ms) Medium-High
E2E Playwright Full user workflows Slow (seconds) High
CodeBuildTestDeployLiveContinuous Integration / Continuous Deployment Pipeline

A typical CI/CD pipeline: code flows through build, test, and deploy stages automatically.

CI/CD Pipeline Configuration

# .gitea/workflows/test.yml
name: Test Suite
on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:unit -- --coverage
      # Runs in ~10 seconds

  integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:integration
      # Requires Docker for Testcontainers, ~2 minutes

  e2e-tests:
    runs-on: ubuntu-latest
    needs: [unit-tests, integration-tests]  # Run after faster tests pass
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run build
      - run: npm run test:e2e
      # Runs in ~5 minutes

At TechSaaS, we follow the testing pyramid for all client projects. Unit tests run locally in watch mode during development, integration tests run in CI with Testcontainers (real databases, real Redis), and E2E tests run as a gate before deployment. The key lesson: do not skip integration tests and rely only on unit tests and E2E. Integration tests catch the bugs that unit tests miss without the fragility of E2E tests.

#testing#unit-testing#integration-testing#e2e#contract-testing

Related Service

Cloud Solutions

Let our experts help you build the right technology strategy for your business.

Need help with tutorials?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.

We Will Build You a Demo Site — For Free

Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.

47+ companies trusted us
99.99% uptime
< 48hr response

No spam. No contracts. Just a free demo.