Testing
01 / 02

Testing Fundamentals & TDD

Testing Fundamentals & TDD

Test Pyramid

          /\
         /  \         E2E Tests (few, slow, brittle)
        /----\        → Playwright, Cypress
       /      \       → full user flows, critical paths
      /--------\
     /          \     Integration Tests (some)
    /------------\    → API routes, DB queries, service layer
   /              \   → real DB or test DB, not mocked
  /----------------\
 /                  \ Unit Tests (many, fast, isolated)
/--------------------\ → pure functions, utils, business logic
                        → heavily mocked dependencies

Rule of thumb: 70% unit, 20% integration, 10% E2E
Avoid: testing implementation details (HOW) — test behaviour (WHAT)

Test-Driven Development (TDD)

  • Red — write a failing test for the desired behaviour

  • Green — write the minimum code to make the test pass

  • Refactor — clean up code without breaking tests

Writing Good Tests (FIRST / AAA)

// AAA pattern: Arrange, Act, Assert
describe('calculateTotal', () => {
  it('applies discount when order exceeds threshold', () => {
    // Arrange
    const items = [{ price: 60 }, { price: 50 }];
    const discountThreshold = 100;
    const discountRate = 0.1;

    // Act
    const total = calculateTotal(items, discountThreshold, discountRate);

    // Assert
    expect(total).toBe(99); // 110 - 10%
  });

  it('returns full price when under threshold', () => {
    const items = [{ price: 40 }, { price: 30 }];
    expect(calculateTotal(items, 100, 0.1)).toBe(70);
  });

  it('throws when items array is empty', () => {
    expect(() => calculateTotal([], 100, 0.1)).toThrow('No items');
  });
});

// FIRST principles:
// Fast       — unit tests should run in milliseconds
// Independent — tests don't share state or order-depend
// Repeatable — same result every run (no random, no time)
// Self-checking — pass/fail without manual inspection
// Timely      — written before or with the code

// Naming: "it should <do X> when <condition>"
// Avoid: "test1", "works correctly", "success case"

Integration & E2E Testing

// Integration test — API route with real DB (test DB)
import request from 'supertest';
import { app } from '../app';
import { db } from '../db';

beforeAll(async () => { await db.migrate.latest(); });
afterAll(async () => { await db.destroy(); });
afterEach(async () => { await db('users').truncate(); });

describe('POST /users', () => {
  it('creates a user and returns 201', async () => {
    const res = await request(app)
      .post('/users')
      .send({ email: 'test@test.com', name: 'Test' });

    expect(res.status).toBe(201);
    expect(res.body).toMatchObject({ email: 'test@test.com' });

    const user = await db('users').where({ email: 'test@test.com' }).first();
    expect(user).toBeDefined();
  });
});

// E2E test — Playwright
import { test, expect } from '@playwright/test';

test('user can sign up and see dashboard', async ({ page }) => {
  await page.goto('/signup');
  await page.fill('[name=email]', 'user@test.com');
  await page.fill('[name=password]', 'Password123!');
  await page.click('[type=submit]');

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Welcome')).toBeVisible();
});

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free