Testing Fundamentals & TDD
Testing Fundamentals & TDD Test Pyramid /\ / \ E2E Tests (few, slow, brittle) /----\ → Playwright, Cypress / \ → full user flows, critical paths /--------\ / \ …
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();
});