COMP 305: Object-Oriented Software Design

University of San Diego, Fall 2025

Test-Driven Development (TDD)

Test-Driven Development is a software development methodology where you write tests before writing the actual code. This approach ensures your code is testable, well-designed, and meets requirements from the start.

Why TDD?

The TDD Cycle: Red-Green-Refactor

1

Red

Write a failing test

2

Green

Make it pass

3

Refactor

Improve the code

The TDD Cycle (Red-Green-Refactor):

1. RED - Write a failing test
   - Write a test for the next bit of functionality
   - Test should fail (no implementation yet)

2. GREEN - Make the test pass
   - Write minimal code to make test pass
   - Don't worry about perfection, just make it work

3. REFACTOR - Improve the code
   - Clean up code while keeping tests green
   - Remove duplication, improve design

Repeat the cycle for each new feature!

TDD Example: Step by Step

Step 1: Red - Write a Failing Test

// Step 1: Write a failing test (RED)
describe('Calculator', () => {
    test('add should sum two numbers', () => {
        const calc = new Calculator();
        expect(calc.add(2, 3)).toBe(5);
    });
});

// This test fails because Calculator doesn't exist yet

Step 2: Green - Make It Pass

// Step 2: Write minimal code to pass (GREEN)
class Calculator {
    add(a, b) {
        return a + b;
    }
}

// Test now passes!

Step 3: Refactor - Improve the Code

// Step 3: Refactor if needed (REFACTOR)
class Calculator {
    add(...numbers) {
        return numbers.reduce((sum, num) => sum + num, 0);
    }
}

// Tests still pass, but code is more flexible!

Complete TDD Workflow Example

Building a password validator using TDD:

// Complete TDD example: Building a Password Validator

// Test 1: Minimum length
test('password must be at least 8 characters', () => {
    const validator = new PasswordValidator();
    expect(validator.isValid('short')).toBe(false);
    expect(validator.isValid('longenough')).toBe(true);
});

// Implementation
class PasswordValidator {
    isValid(password) {
        return password.length >= 8;
    }
}

// Test 2: Must contain uppercase
test('password must contain uppercase letter', () => {
    const validator = new PasswordValidator();
    expect(validator.isValid('lowercase')).toBe(false);
    expect(validator.isValid('Uppercase')).toBe(true);
});

// Implementation (expand)
class PasswordValidator {
    isValid(password) {
        if (password.length < 8) return false;
        if (!/[A-Z]/.test(password)) return false;
        return true;
    }
}

// Test 3: Must contain number
test('password must contain a number', () => {
    const validator = new PasswordValidator();
    expect(validator.isValid('NoNumbers')).toBe(false);
    expect(validator.isValid('HasNumber1')).toBe(true);
});

// Implementation (expand)
class PasswordValidator {
    isValid(password) {
        if (password.length < 8) return false;
        if (!/[A-Z]/.test(password)) return false;
        if (!/\d/.test(password)) return false;
        return true;
    }
}

// Refactor: Make it cleaner
class PasswordValidator {
    constructor() {
        this.rules = [
            { test: (pwd) => pwd.length >= 8, message: 'Too short' },
            { test: (pwd) => /[A-Z]/.test(pwd), message: 'No uppercase' },
            { test: (pwd) => /\d/.test(pwd), message: 'No number' }
        ];
    }

    isValid(password) {
        return this.rules.every(rule => rule.test(password));
    }

    getErrors(password) {
        return this.rules
            .filter(rule => !rule.test(password))
            .map(rule => rule.message);
    }
}

Writing Good Tests

// Best practices for writing tests

// 1. Descriptive test names
test('throws error when dividing by zero', () => {
    // Clear what's being tested
});

// 2. Arrange-Act-Assert pattern
test('calculates discount correctly', () => {
    // Arrange - set up test data
    const calculator = new DiscountCalculator();
    const price = 100;

    // Act - perform action
    const result = calculator.applyDiscount(price, 0.2);

    // Assert - verify result
    expect(result).toBe(80);
});

// 3. Test one thing at a time
test('validates email format', () => {
    const validator = new EmailValidator();
    expect(validator.isValid('test@example.com')).toBe(true);
});

test('rejects invalid email', () => {
    const validator = new EmailValidator();
    expect(validator.isValid('notanemail')).toBe(false);
});

// 4. Use meaningful assertions
expect(user.age).toBe(25);                    // Exact value
expect(users).toHaveLength(3);                // Array length
expect(error).toContain('Invalid');           // String contains
expect(callback).toHaveBeenCalledWith(42);    // Function called
expect(promise).resolves.toBe('success');     // Async result

Benefits of TDD

Common Challenges

TDD Best Practices

When to Use TDD

When TDD May Not Fit:

Testing Frameworks

Resources