JavaScript Testing and Code Quality

1. Unit Testing Strategies and Test Structure

Unit Test Principles

Principle Description Benefit
Isolation Test one unit independently Pinpoint failures
Fast Run quickly (<100ms each) Rapid feedback
Repeatable Same result every time Reliable CI/CD
Self-validating Pass or fail, no manual check Automation friendly
Timely Written before/with code Better design

Test Structure (AAA Pattern)

Phase Purpose Activities
Arrange Setup test preconditions Create objects, set state, prepare inputs
Act Execute the code under test Call method, trigger action
Assert Verify expected outcome Check results, validate state

Test Naming Conventions

Pattern Format Example
MethodName_StateUnderTest_ExpectedBehavior method_condition_result divide_byZero_throwsError
should_ExpectedBehavior_When_StateUnderTest should_result_when_condition should_throwError_when_dividingByZero
Given_When_Then given_when_then givenEmptyCart_whenAddingItem_thenCartHasOneItem
Descriptive sentence Natural language returns sum of two numbers

Test Organization

Level Purpose Syntax
describe Group related tests describe('Calculator', () => {})
it/test Individual test case it('adds two numbers', () => {})
beforeEach Setup before each test beforeEach(() => {})
afterEach Cleanup after each test afterEach(() => {})
beforeAll Setup once before all tests beforeAll(() => {})
afterAll Cleanup once after all tests afterAll(() => {})

Example: Unit test structure

// Function to test
function add(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
        throw new Error('Arguments must be numbers');
    }
    return a + b;
}

function divide(a, b) {
    if (b === 0) {
        throw new Error('Division by zero');
    }
    return a / b;
}

// Unit tests using Jest/Vitest syntax
describe('Math operations', () => {
    // Test suite for add function
    describe('add', () => {
        // AAA Pattern: Arrange, Act, Assert
        it('should add two positive numbers', () => {
            // Arrange
            const a = 2;
            const b = 3;
            
            // Act
            const result = add(a, b);
            
            // Assert
            expect(result).toBe(5);
        });
        
        it('should add negative numbers', () => {
            expect(add(-2, -3)).toBe(-5);
        });
        
        it('should handle zero', () => {
            expect(add(5, 0)).toBe(5);
            expect(add(0, 5)).toBe(5);
        });
        
        it('should throw error for non-number inputs', () => {
            expect(() => add('2', 3)).toThrow('Arguments must be numbers');
            expect(() => add(2, null)).toThrow('Arguments must be numbers');
        });
    });
    
    // Test suite for divide function
    describe('divide', () => {
        it('should divide two numbers', () => {
            expect(divide(10, 2)).toBe(5);
        });
        
        it('should handle decimals', () => {
            expect(divide(5, 2)).toBe(2.5);
        });
        
        it('should throw error when dividing by zero', () => {
            expect(() => divide(10, 0)).toThrow('Division by zero');
        });
    });
});

// Testing a class
class Calculator {
    constructor() {
        this.result = 0;
    }
    
    add(n) {
        this.result += n;
        return this;
    }
    
    subtract(n) {
        this.result -= n;
        return this;
    }
    
    multiply(n) {
        this.result *= n;
        return this;
    }
    
    getResult() {
        return this.result;
    }
    
    clear() {
        this.result = 0;
        return this;
    }
}

describe('Calculator', () => {
    let calculator;
    
    // Setup before each test
    beforeEach(() => {
        calculator = new Calculator();
    });
    
    // Cleanup after each test (if needed)
    afterEach(() => {
        calculator = null;
    });
    
    describe('add', () => {
        it('should add number to result', () => {
            calculator.add(5);
            expect(calculator.getResult()).toBe(5);
        });
        
        it('should support method chaining', () => {
            calculator.add(5).add(3);
            expect(calculator.getResult()).toBe(8);
        });
    });
    
    describe('subtract', () => {
        it('should subtract number from result', () => {
            calculator.add(10).subtract(3);
            expect(calculator.getResult()).toBe(7);
        });
    });
    
    describe('multiply', () => {
        it('should multiply result by number', () => {
            calculator.add(5).multiply(3);
            expect(calculator.getResult()).toBe(15);
        });
    });
    
    describe('clear', () => {
        it('should reset result to zero', () => {
            calculator.add(10).clear();
            expect(calculator.getResult()).toBe(0);
        });
    });
    
    describe('complex operations', () => {
        it('should handle chained operations', () => {
            calculator
                .add(10)
                .subtract(3)
                .multiply(2);
            
            expect(calculator.getResult()).toBe(14);
        });
    });
});

// Testing async functions
async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
        throw new Error('User not found');
    }
    return response.json();
}

describe('fetchUser', () => {
    it('should fetch user data', async () => {
        // Mock fetch
        global.fetch = jest.fn(() =>
            Promise.resolve({
                ok: true,
                json: () => Promise.resolve({id: 1, name: 'John'})
            })
        );
        
        const user = await fetchUser(1);
        
        expect(user).toEqual({id: 1, name: 'John'});
        expect(fetch).toHaveBeenCalledWith('/api/users/1');
    });
    
    it('should throw error when user not found', async () => {
        global.fetch = jest.fn(() =>
            Promise.resolve({
                ok: false
            })
        );
        
        await expect(fetchUser(999)).rejects.toThrow('User not found');
    });
});

// Parameterized tests (test.each)
describe('add with multiple inputs', () => {
    test.each([
        [1, 1, 2],
        [2, 3, 5],
        [5, -5, 0],
        [-10, -20, -30]
    ])('add(%i, %i) should return %i', (a, b, expected) => {
        expect(add(a, b)).toBe(expected);
    });
});

// Testing edge cases
describe('Edge cases', () => {
    it('should handle large numbers', () => {
        expect(add(Number.MAX_SAFE_INTEGER, 1))
            .toBe(Number.MAX_SAFE_INTEGER + 1);
    });
    
    it('should handle floating point precision', () => {
        expect(add(0.1, 0.2)).toBeCloseTo(0.3);
    });
    
    it('should handle negative zero', () => {
        expect(add(-0, 0)).toBe(0);
    });
});

// Snapshot testing (for objects/arrays)
describe('User object', () => {
    it('should match snapshot', () => {
        const user = {
            id: 1,
            name: 'John',
            email: 'john@example.com',
            createdAt: new Date('2024-01-01')
        };
        
        expect(user).toMatchSnapshot();
    });
});

// Testing with setup and teardown
describe('Database operations', () => {
    let db;
    
    beforeAll(async () => {
        // Setup: connect to test database
        db = await connectToTestDB();
    });
    
    afterAll(async () => {
        // Teardown: disconnect from database
        await db.disconnect();
    });
    
    beforeEach(async () => {
        // Reset database state before each test
        await db.clear();
    });
    
    it('should insert user', async () => {
        const user = await db.users.insert({name: 'John'});
        expect(user.id).toBeDefined();
        expect(user.name).toBe('John');
    });
    
    it('should find user by id', async () => {
        const inserted = await db.users.insert({name: 'Jane'});
        const found = await db.users.findById(inserted.id);
        expect(found).toEqual(inserted);
    });
});

// Skip and only for focused testing
describe('Feature tests', () => {
    it('should run this test', () => {
        expect(true).toBe(true);
    });
    
    it.skip('should skip this test', () => {
        // This test will be skipped
        expect(false).toBe(true);
    });
    
    it.only('should only run this test', () => {
        // Only this test will run (useful for debugging)
        expect(true).toBe(true);
    });
});
Key Points: Unit tests test individual units in isolation. Follow AAA pattern: Arrange, Act, Assert. Use descriptive test names. Organize with describe/it blocks. Setup/teardown with beforeEach/afterEach. Test edge cases, error conditions. Use test.each for parameterized tests. Keep tests fast (<100ms), repeatable, independent. One assertion per test (generally).

2. Mocking and Stubbing Techniques

Test Doubles Types

Type Purpose Usage
Mock Verify interactions Track calls, arguments, return values
Stub Provide predefined responses Return fixed values
Spy Wrap real function Track calls but execute real code
Fake Working implementation Simplified version (in-memory DB)
Dummy Placeholder Passed but never used

Jest Mock Methods

Method Purpose Example
jest.fn() Create mock function const mock = jest.fn()
jest.spyOn() Spy on existing method jest.spyOn(obj, 'method')
mockReturnValue() Set return value mock.mockReturnValue(42)
mockResolvedValue() Resolve with value mock.mockResolvedValue(data)
mockRejectedValue() Reject with error mock.mockRejectedValue(error)
mockImplementation() Custom implementation mock.mockImplementation(fn)

Mock Assertions

Assertion Purpose Usage
toHaveBeenCalled() Verify called expect(mock).toHaveBeenCalled()
toHaveBeenCalledTimes() Verify call count expect(mock).toHaveBeenCalledTimes(3)
toHaveBeenCalledWith() Verify arguments expect(mock).toHaveBeenCalledWith(arg1, arg2)
toHaveBeenLastCalledWith() Verify last call arguments expect(mock).toHaveBeenLastCalledWith(arg)
toHaveReturnedWith() Verify return value expect(mock).toHaveReturnedWith(42)

Example: Mocking and stubbing

// Function using dependencies
class UserService {
    constructor(api, logger) {
        this.api = api;
        this.logger = logger;
    }
    
    async getUser(id) {
        this.logger.log(`Fetching user ${id}`);
        
        try {
            const user = await this.api.get(`/users/${id}`);
            this.logger.log(`User ${id} fetched successfully`);
            return user;
        } catch (error) {
            this.logger.error(`Failed to fetch user ${id}`, error);
            throw error;
        }
    }
    
    async createUser(data) {
        this.logger.log('Creating user');
        return this.api.post('/users', data);
    }
}

// Testing with mocks
describe('UserService', () => {
    let userService;
    let mockApi;
    let mockLogger;
    
    beforeEach(() => {
        // Create mock objects
        mockApi = {
            get: jest.fn(),
            post: jest.fn()
        };
        
        mockLogger = {
            log: jest.fn(),
            error: jest.fn()
        };
        
        userService = new UserService(mockApi, mockLogger);
    });
    
    describe('getUser', () => {
        it('should fetch user from API', async () => {
            // Arrange
            const mockUser = {id: 1, name: 'John'};
            mockApi.get.mockResolvedValue(mockUser);
            
            // Act
            const result = await userService.getUser(1);
            
            // Assert
            expect(result).toEqual(mockUser);
            expect(mockApi.get).toHaveBeenCalledWith('/users/1');
            expect(mockLogger.log).toHaveBeenCalledWith('Fetching user 1');
            expect(mockLogger.log).toHaveBeenCalledWith('User 1 fetched successfully');
        });
        
        it('should log error when fetch fails', async () => {
            // Arrange
            const error = new Error('Network error');
            mockApi.get.mockRejectedValue(error);
            
            // Act & Assert
            await expect(userService.getUser(1)).rejects.toThrow('Network error');
            expect(mockLogger.error).toHaveBeenCalledWith(
                'Failed to fetch user 1',
                error
            );
        });
    });
    
    describe('createUser', () => {
        it('should create user via API', async () => {
            const userData = {name: 'Jane', email: 'jane@example.com'};
            const createdUser = {id: 2, ...userData};
            
            mockApi.post.mockResolvedValue(createdUser);
            
            const result = await userService.createUser(userData);
            
            expect(result).toEqual(createdUser);
            expect(mockApi.post).toHaveBeenCalledWith('/users', userData);
            expect(mockLogger.log).toHaveBeenCalledWith('Creating user');
        });
    });
});

// Spying on existing methods
describe('Math operations with spy', () => {
    it('should spy on Math.random', () => {
        const spy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
        
        const result = Math.random();
        
        expect(result).toBe(0.5);
        expect(spy).toHaveBeenCalled();
        
        spy.mockRestore(); // Restore original implementation
    });
    
    it('should spy on console.log', () => {
        const spy = jest.spyOn(console, 'log').mockImplementation();
        
        console.log('Hello');
        console.log('World');
        
        expect(spy).toHaveBeenCalledTimes(2);
        expect(spy).toHaveBeenCalledWith('Hello');
        expect(spy).toHaveBeenCalledWith('World');
        
        spy.mockRestore();
    });
});

// Mock implementations
describe('Mock implementations', () => {
    it('should use custom implementation', () => {
        const mockFn = jest.fn((a, b) => a + b);
        
        expect(mockFn(2, 3)).toBe(5);
        expect(mockFn(5, 7)).toBe(12);
        expect(mockFn).toHaveBeenCalledTimes(2);
    });
    
    it('should return different values per call', () => {
        const mockFn = jest.fn()
            .mockReturnValueOnce('first')
            .mockReturnValueOnce('second')
            .mockReturnValue('default');
        
        expect(mockFn()).toBe('first');
        expect(mockFn()).toBe('second');
        expect(mockFn()).toBe('default');
        expect(mockFn()).toBe('default');
    });
    
    it('should handle async operations', async () => {
        const mockFn = jest.fn()
            .mockResolvedValueOnce({id: 1})
            .mockRejectedValueOnce(new Error('Failed'));
        
        await expect(mockFn()).resolves.toEqual({id: 1});
        await expect(mockFn()).rejects.toThrow('Failed');
    });
});

// Mocking modules
// math.js
function add2(a, b) {
    return a + b;
}

function multiply2(a, b) {
    return a * b;
}

// calculator.js
function calculate2(operation, a, b) {
    if (operation === 'add') {
        return add2(a, b);
    } else if (operation === 'multiply') {
        return multiply2(a, b);
    }
}

// calculator.test.js
jest.mock('./math', () => ({
    add: jest.fn((a, b) => 100),  // Always return 100
    multiply: jest.fn((a, b) => 200)
}));

describe('calculator with mocked module', () => {
    it('should use mocked add', () => {
        const result = calculate2('add', 2, 3);
        expect(result).toBe(100); // Uses mocked value
    });
});

// Partial mocking
jest.mock('./math', () => {
    const actual = jest.requireActual('./math');
    return {
        ...actual,
        add: jest.fn((a, b) => 100) // Only mock add, keep multiply real
    };
});

// Timer mocks
describe('Timer mocks', () => {
    beforeEach(() => {
        jest.useFakeTimers();
    });
    
    afterEach(() => {
        jest.useRealTimers();
    });
    
    it('should execute after timeout', () => {
        const callback = jest.fn();
        
        setTimeout(callback, 1000);
        
        expect(callback).not.toHaveBeenCalled();
        
        jest.advanceTimersByTime(1000);
        
        expect(callback).toHaveBeenCalled();
    });
    
    it('should execute interval', () => {
        const callback = jest.fn();
        
        setInterval(callback, 1000);
        
        jest.advanceTimersByTime(3000);
        
        expect(callback).toHaveBeenCalledTimes(3);
    });
});

// Mocking fetch
describe('Fetch mocking', () => {
    beforeEach(() => {
        global.fetch = jest.fn();
    });
    
    it('should fetch user data', async () => {
        const mockUser = {id: 1, name: 'John'};
        
        global.fetch.mockResolvedValue({
            ok: true,
            json: async () => mockUser
        });
        
        const response = await fetch('/api/users/1');
        const data = await response.json();
        
        expect(data).toEqual(mockUser);
        expect(fetch).toHaveBeenCalledWith('/api/users/1');
    });
    
    it('should handle fetch error', async () => {
        global.fetch.mockResolvedValue({
            ok: false,
            status: 404
        });
        
        const response = await fetch('/api/users/999');
        
        expect(response.ok).toBe(false);
        expect(response.status).toBe(404);
    });
});

// Manual mocks
class MockLocalStorage {
    constructor() {
        this.store = {};
    }
    
    getItem(key) {
        return this.store[key] || null;
    }
    
    setItem(key, value) {
        this.store[key] = value.toString();
    }
    
    removeItem(key) {
        delete this.store[key];
    }
    
    clear() {
        this.store = {};
    }
}

describe('LocalStorage operations', () => {
    let mockStorage;
    
    beforeEach(() => {
        mockStorage = new MockLocalStorage();
        global.localStorage = mockStorage;
    });
    
    it('should store and retrieve item', () => {
        localStorage.setItem('key', 'value');
        expect(localStorage.getItem('key')).toBe('value');
    });
    
    it('should remove item', () => {
        localStorage.setItem('key', 'value');
        localStorage.removeItem('key');
        expect(localStorage.getItem('key')).toBeNull();
    });
});
Key Points: Mocks verify interactions (calls, arguments). Stubs provide fixed responses. Spies track calls while executing real code. Use jest.fn() for mocks, jest.spyOn() for spies. Mock return values with mockReturnValue, mockResolvedValue. Assert with toHaveBeenCalled, toHaveBeenCalledWith. Mock modules with jest.mock(). Use fake timers for testing setTimeout/setInterval.

3. Test-Driven Development (TDD) Patterns

TDD Cycle (Red-Green-Refactor)

Phase Action Goal
Red Write failing test Define expected behavior
Green Write minimal code to pass Make test pass quickly
Refactor Improve code quality Clean up while maintaining tests

TDD Benefits

Benefit Description Impact
Better Design Forces thinking about API first More modular, testable code
Documentation Tests document expected behavior Living specifications
Confidence Safety net for changes Fearless refactoring
Regression Prevention Catch bugs early Fewer production issues
Focus One requirement at a time Incremental progress

TDD Best Practices

Practice Description Example
Small Steps Test one thing at a time One assertion per test
Test First Always write test before code Define API through tests
Simplest Solution Write minimal code to pass Avoid over-engineering
Refactor Often Clean up after green Remove duplication, improve names
Fast Feedback Run tests frequently Catch issues immediately

Example: TDD workflow

// Example: Building a shopping cart with TDD

// STEP 1: RED - Write failing test
describe('ShoppingCart', () => {
    it('should start empty', () => {
        const cart = new ShoppingCart();
        expect(cart.getItemCount()).toBe(0);
    });
});

// Run test → FAIL (ShoppingCart doesn't exist)

// STEP 2: GREEN - Write minimal code to pass
class ShoppingCart {
    constructor() {
        this.items = [];
    }
    
    getItemCount() {
        return 0; // Hardcoded to pass test
    }
}

// Run test → PASS

// STEP 3: REFACTOR - (none needed yet)

// STEP 4: RED - Next test
describe('ShoppingCart', () => {
    it('should start empty', () => {
        const cart = new ShoppingCart();
        expect(cart.getItemCount()).toBe(0);
    });
    
    it('should add item to cart', () => {
        const cart = new ShoppingCart();
        cart.addItem({name: 'Apple', price: 1.5});
        expect(cart.getItemCount()).toBe(1);
    });
});

// Run test → FAIL (addItem doesn't exist)

// STEP 5: GREEN - Implement addItem
class ShoppingCart2 {
    constructor() {
        this.items = [];
    }
    
    addItem(item) {
        this.items.push(item);
    }
    
    getItemCount() {
        return this.items.length; // Now dynamic
    }
}

// Run test → PASS

// STEP 6: RED - Test multiple items
it('should add multiple items', () => {
    const cart = new ShoppingCart2();
    cart.addItem({name: 'Apple', price: 1.5});
    cart.addItem({name: 'Banana', price: 2.0});
    expect(cart.getItemCount()).toBe(2);
});

// Run test → PASS (already works!)

// STEP 7: RED - Test total calculation
it('should calculate total', () => {
    const cart = new ShoppingCart2();
    cart.addItem({name: 'Apple', price: 1.5});
    cart.addItem({name: 'Banana', price: 2.0});
    expect(cart.getTotal()).toBe(3.5);
});

// Run test → FAIL (getTotal doesn't exist)

// STEP 8: GREEN - Implement getTotal
class ShoppingCart3 {
    constructor() {
        this.items = [];
    }
    
    addItem(item) {
        this.items.push(item);
    }
    
    getItemCount() {
        return this.items.length;
    }
    
    getTotal() {
        return this.items.reduce((sum, item) => sum + item.price, 0);
    }
}

// Run test → PASS

// STEP 9: RED - Test remove item
it('should remove item from cart', () => {
    const cart = new ShoppingCart3();
    cart.addItem({id: 1, name: 'Apple', price: 1.5});
    cart.addItem({id: 2, name: 'Banana', price: 2.0});
    
    cart.removeItem(1);
    
    expect(cart.getItemCount()).toBe(1);
    expect(cart.getTotal()).toBe(2.0);
});

// Run test → FAIL (removeItem doesn't exist)

// STEP 10: GREEN - Implement removeItem
class ShoppingCart4 {
    constructor() {
        this.items = [];
    }
    
    addItem(item) {
        this.items.push(item);
    }
    
    removeItem(id) {
        this.items = this.items.filter(item => item.id !== id);
    }
    
    getItemCount() {
        return this.items.length;
    }
    
    getTotal() {
        return this.items.reduce((sum, item) => sum + item.price, 0);
    }
}

// Run test → PASS

// STEP 11: REFACTOR - Extract duplicated test setup
describe('ShoppingCart (refactored)', () => {
    let cart;
    
    beforeEach(() => {
        cart = new ShoppingCart4();
    });
    
    it('should start empty', () => {
        expect(cart.getItemCount()).toBe(0);
    });
    
    describe('addItem', () => {
        it('should add single item', () => {
            cart.addItem({id: 1, name: 'Apple', price: 1.5});
            expect(cart.getItemCount()).toBe(1);
        });
        
        it('should add multiple items', () => {
            cart.addItem({id: 1, name: 'Apple', price: 1.5});
            cart.addItem({id: 2, name: 'Banana', price: 2.0});
            expect(cart.getItemCount()).toBe(2);
        });
    });
    
    describe('getTotal', () => {
        it('should calculate total for multiple items', () => {
            cart.addItem({id: 1, name: 'Apple', price: 1.5});
            cart.addItem({id: 2, name: 'Banana', price: 2.0});
            expect(cart.getTotal()).toBe(3.5);
        });
        
        it('should return 0 for empty cart', () => {
            expect(cart.getTotal()).toBe(0);
        });
    });
    
    describe('removeItem', () => {
        beforeEach(() => {
            cart.addItem({id: 1, name: 'Apple', price: 1.5});
            cart.addItem({id: 2, name: 'Banana', price: 2.0});
        });
        
        it('should remove item', () => {
            cart.removeItem(1);
            expect(cart.getItemCount()).toBe(1);
            expect(cart.getTotal()).toBe(2.0);
        });
        
        it('should handle removing non-existent item', () => {
            cart.removeItem(999);
            expect(cart.getItemCount()).toBe(2);
        });
    });
});

// TDD with edge cases
describe('ShoppingCart edge cases', () => {
    let cart;
    
    beforeEach(() => {
        cart = new ShoppingCart4();
    });
    
    it('should handle item with quantity', () => {
        cart.addItem({id: 1, name: 'Apple', price: 1.5, quantity: 3});
        
        // This test will fail, driving us to add quantity support
        expect(cart.getTotal()).toBe(4.5); // 1.5 * 3
    });
    
    it('should apply discount', () => {
        cart.addItem({id: 1, name: 'Apple', price: 10});
        cart.applyDiscount(0.1); // 10% discount
        
        expect(cart.getTotal()).toBe(9);
    });
    
    it('should clear cart', () => {
        cart.addItem({id: 1, name: 'Apple', price: 1.5});
        cart.clear();
        
        expect(cart.getItemCount()).toBe(0);
        expect(cart.getTotal()).toBe(0);
    });
});

// Test-first for bug fixes
describe('Bug fix: duplicate items', () => {
    it('should prevent adding duplicate items', () => {
        const cart = new ShoppingCart4();
        
        cart.addItem({id: 1, name: 'Apple', price: 1.5});
        cart.addItem({id: 1, name: 'Apple', price: 1.5}); // Duplicate
        
        expect(cart.getItemCount()).toBe(1); // Should only have 1 item
    });
    
    // This test fails, so we fix the implementation
});

// Implementation with duplicate prevention
class ShoppingCart5 {
    constructor() {
        this.items = [];
    }
    
    addItem(item) {
        const exists = this.items.find(i => i.id === item.id);
        if (!exists) {
            this.items.push(item);
        }
    }
    
    // ... rest of methods
}
Key Points: TDD cycle: Red (write failing test), Green (make it pass), Refactor (improve code). Write test first to define API. Write minimal code to pass test. Refactor with safety of tests. Benefits: better design, documentation, confidence. Test one thing at a time. Run tests frequently for fast feedback. Use TDD for bug fixes: write test that reproduces bug, fix code, test passes.

4. Code Coverage and Quality Metrics

Coverage Types

Type Measures Goal
Line Coverage % of lines executed 80-90% typically
Branch Coverage % of if/else branches tested More thorough than line
Function Coverage % of functions called All functions tested
Statement Coverage % of statements executed Similar to line coverage

Quality Metrics

Metric Description Tool
Cyclomatic Complexity Number of execution paths ESLint, SonarQube
Maintainability Index Ease of maintenance score Code Climate
Code Duplication % of duplicated code SonarQube, jscpd
Technical Debt Time to fix issues SonarQube
Test Quality Mutation score, assertion density Stryker

Coverage Tools

Tool Purpose Usage
Istanbul/nyc Code coverage reporting jest --coverage
Codecov Coverage tracking service CI/CD integration
Coveralls Coverage history tracking GitHub integration
SonarQube Quality metrics platform Enterprise quality gate

Example: Code coverage

// Function with branches
function getDiscount(customer) {
    if (customer.isPremium) {
        if (customer.purchaseAmount > 100) {
            return 0.2; // 20% discount
        } else {
            return 0.1; // 10% discount
        }
    } else {
        if (customer.purchaseAmount > 50) {
            return 0.05; // 5% discount
        } else {
            return 0; // No discount
        }
    }
}

// Tests for 100% branch coverage
describe('getDiscount', () => {
    it('should give 20% discount for premium customer with high purchase', () => {
        const customer = {isPremium: true, purchaseAmount: 150};
        expect(getDiscount(customer)).toBe(0.2);
    });
    
    it('should give 10% discount for premium customer with low purchase', () => {
        const customer = {isPremium: true, purchaseAmount: 50};
        expect(getDiscount(customer)).toBe(0.1);
    });
    
    it('should give 5% discount for non-premium with medium purchase', () => {
        const customer = {isPremium: false, purchaseAmount: 75};
        expect(getDiscount(customer)).toBe(0.05);
    });
    
    it('should give no discount for non-premium with low purchase', () => {
        const customer = {isPremium: false, purchaseAmount: 25};
        expect(getDiscount(customer)).toBe(0);
    });
});

// All 4 branches are now covered!

// Running coverage with Jest
// package.json
{
    "scripts": {
        "test": "jest",
        "test:coverage": "jest --coverage",
        "test:watch": "jest --watch",
        "test:coverage:watch": "jest --coverage --watch"
    },
    "jest": {
        "collectCoverageFrom": [
            "src/**/*.js",
            "!src/**/*.test.js",
            "!src/index.js"
        ],
        "coverageThreshold": {
            "global": {
                "branches": 80,
                "functions": 80,
                "lines": 80,
                "statements": 80
            }
        }
    }
}

// Coverage report shows:
// File      | % Stmts | % Branch | % Funcs | % Lines |
// ----------|---------|----------|---------|---------|
// discount.js |  100    |   100    |   100   |   100   |

// Uncovered code example
function processPayment(amount, method) {
    if (method === 'credit') {
        return chargeCreditCard(amount);
    } else if (method === 'debit') {
        return chargeDebitCard(amount);
    } else if (method === 'paypal') {
        return chargePayPal(amount);
    }
    // No else branch - what if method is invalid?
    // Coverage report will show this as uncovered
}

// Better version with default case
function processPayment2(amount, method) {
    if (method === 'credit') {
        return chargeCreditCard(amount);
    } else if (method === 'debit') {
        return chargeDebitCard(amount);
    } else if (method === 'paypal') {
        return chargePayPal(amount);
    } else {
        throw new Error(`Unknown payment method: ${method}`);
    }
}

// Test for error case
it('should throw error for invalid payment method', () => {
    expect(() => processPayment2(100, 'bitcoin'))
        .toThrow('Unknown payment method: bitcoin');
});

// Cyclomatic complexity example
// Bad: High complexity (hard to test, maintain)
function calculateShipping(weight, distance, isPremium, isInternational, isFragile) {
    let cost = 0;
    
    if (weight > 10) {
        if (distance > 100) {
            if (isPremium) {
                if (isInternational) {
                    if (isFragile) {
                        cost = 100;
                    } else {
                        cost = 80;
                    }
                } else {
                    cost = 50;
                }
            } else {
                cost = 30;
            }
        } else {
            cost = 20;
        }
    } else {
        cost = 10;
    }
    
    return cost;
}
// Cyclomatic complexity: 7 (too high!)

// Good: Lower complexity (easier to test, maintain)
function calculateShipping2(options) {
    const {weight, distance, isPremium, isInternational, isFragile} = options;
    
    let cost = getBaseCost(weight, distance);
    
    if (isPremium) {
        cost = applyPremiumRate(cost);
    }
    
    if (isInternational) {
        cost = applyInternationalRate(cost);
    }
    
    if (isFragile) {
        cost = applyFragileHandling(cost);
    }
    
    return cost;
}

function getBaseCost(weight, distance) {
    if (weight > 10 && distance > 100) return 30;
    if (weight > 10) return 20;
    return 10;
}

function applyPremiumRate(cost) {
    return cost * 1.5;
}

function applyInternationalRate(cost) {
    return cost * 2;
}

function applyFragileHandling(cost) {
    return cost + 20;
}
// Each function has lower complexity, easier to test individually

// Mutation testing concept
function isPositive(n) {
    return n > 0;  // Mutant: change > to >=
}

// This test would NOT catch the mutation
it('should return true for positive number', () => {
    expect(isPositive(5)).toBe(true);
});

// Better test suite that catches mutations
describe('isPositive', () => {
    it('should return true for positive number', () => {
        expect(isPositive(5)).toBe(true);
    });
    
    it('should return false for zero', () => {
        expect(isPositive(0)).toBe(false); // Catches >= mutation
    });
    
    it('should return false for negative number', () => {
        expect(isPositive(-5)).toBe(false);
    });
});

// Coverage doesn't guarantee quality
function divide2(a, b) {
    return a / b;
}

// 100% coverage, but missing important test
it('divides two numbers', () => {
    expect(divide2(10, 2)).toBe(5);
});
// No test for division by zero!

// Better test suite
describe('divide', () => {
    it('should divide two numbers', () => {
        expect(divide2(10, 2)).toBe(5);
    });
    
    it('should handle division by zero', () => {
        expect(divide2(10, 0)).toBe(Infinity);
    });
    
    it('should handle negative numbers', () => {
        expect(divide2(-10, 2)).toBe(-5);
    });
    
    it('should handle decimals', () => {
        expect(divide2(5, 2)).toBe(2.5);
    });
});

// Code quality checks with ESLint
// .eslintrc.json
{
    "extends": "eslint:recommended",
    "rules": {
        "complexity": ["error", 5],  // Max cyclomatic complexity
        "max-depth": ["error", 3],   // Max nesting depth
        "max-lines-per-function": ["warn", 50],
        "max-params": ["error", 4],  // Max function parameters
        "no-duplicate-code": "error"
    }
}

// Measuring test quality
describe('Test metrics', () => {
    // Number of assertions per test
    it('should have focused assertions', () => {
        const user = createUser();
        expect(user.name).toBeDefined();  // 1
        expect(user.email).toBeDefined(); // 2
        expect(user.id).toBeDefined();    // 3
        // 3 assertions per test
    });
    
    // Test execution time
    it('should run fast', () => {
        const start = performance.now();
        
        const result = someOperation();
        
        const duration = performance.now() - start;
        expect(duration).toBeLessThan(100); // Should be fast
    });
});

// Coverage configuration for different file types
// jest.config.js
module.exports = {
    collectCoverageFrom: [
        'src/**/*.{js,jsx,ts,tsx}',
        '!src/**/*.d.ts',
        '!src/**/*.test.{js,jsx,ts,tsx}',
        '!src/**/__tests__/**',
        '!src/index.js',
        '!src/setupTests.js'
    ],
    coverageThreshold: {
        global: {
            branches: 80,
            functions: 80,
            lines: 80,
            statements: 80
        },
        './src/utils/': {
            branches: 90,
            functions: 90,
            lines: 90,
            statements: 90
        }
    },
    coverageReporters: ['text', 'lcov', 'html']
};
Key Points: Code coverage measures % of code executed by tests. Types: line, branch, function, statement coverage. Aim for 80-90% coverage. Branch coverage more thorough than line coverage. Coverage doesn't guarantee quality - need good assertions. Cyclomatic complexity measures code complexity. Mutation testing validates test quality. Use ESLint for complexity limits. Configure coverage thresholds in jest.config.

5. Assertion Libraries (expect, assert) and Testing Utilities

Common Assertion Libraries

Library Style Usage
Jest expect() Built-in, batteries included
Chai expect/should/assert Flexible, plugin ecosystem
Vitest expect() (Jest-compatible) Vite-native, fast
Node assert assert.equal() Built-in Node.js

Jest Matchers

Matcher Purpose Example
toBe Strict equality (===) expect(value).toBe(5)
toEqual Deep equality expect(obj).toEqual({a: 1})
toMatch Regex match expect(str).toMatch(/hello/)
toContain Array/string contains expect(arr).toContain(item)
toThrow Function throws error expect(fn).toThrow()
toBeTruthy/toBeFalsy Boolean coercion expect(value).toBeTruthy()
toBeNull/toBeUndefined Null/undefined check expect(val).toBeNull()
toBeGreaterThan Numeric comparison expect(5).toBeGreaterThan(3)
toHaveLength Array/string length expect(arr).toHaveLength(3)
toHaveProperty Object property check expect(obj).toHaveProperty('key')

Testing Utilities

Utility Purpose Library
Faker Generate fake data @faker-js/faker
Testing Library DOM testing utilities @testing-library/react
MSW Mock service worker (API mocking) msw
Supertest HTTP assertion supertest
Nock HTTP mocking nock

Example: Assertions and matchers

// Basic matchers
describe('Basic matchers', () => {
    it('toBe for primitives (===)', () => {
        expect(2 + 2).toBe(4);
        expect('hello').toBe('hello');
        expect(true).toBe(true);
    });
    
    it('toEqual for objects/arrays (deep equality)', () => {
        const obj1 = {name: 'John', age: 30};
        const obj2 = {name: 'John', age: 30};
        
        expect(obj1).toEqual(obj2);  // Pass
        // expect(obj1).toBe(obj2);  // Fail (different references)
        
        expect([1, 2, 3]).toEqual([1, 2, 3]);
    });
    
    it('toStrictEqual (stricter than toEqual)', () => {
        const obj = {a: 1, b: undefined};
        
        expect(obj).toEqual({a: 1});        // Pass (ignores undefined)
        expect(obj).toStrictEqual({a: 1});  // Fail (checks undefined)
    });
});

// Number matchers
describe('Number matchers', () => {
    it('comparison matchers', () => {
        expect(5).toBeGreaterThan(3);
        expect(3).toBeLessThan(5);
        expect(5).toBeGreaterThanOrEqual(5);
        expect(3).toBeLessThanOrEqual(3);
    });
    
    it('toBeCloseTo for floating point', () => {
        expect(0.1 + 0.2).toBeCloseTo(0.3);  // Handles floating point precision
        expect(0.1 + 0.2).toBeCloseTo(0.3, 5);  // 5 decimal places
    });
});

// String matchers
describe('String matchers', () => {
    it('toMatch with regex', () => {
        expect('hello world').toMatch(/world/);
        expect('hello world').toMatch(/^hello/);
        expect('test@example.com').toMatch(/^[^@]+@[^@]+\.[^@]+$/);
    });
    
    it('string contains', () => {
        expect('hello world').toContain('world');
    });
});

// Array matchers
describe('Array matchers', () => {
    it('toContain for arrays', () => {
        const arr = [1, 2, 3, 4, 5];
        expect(arr).toContain(3);
        expect(arr).toContain(5);
    });
    
    it('toHaveLength', () => {
        expect([1, 2, 3]).toHaveLength(3);
        expect('hello').toHaveLength(5);
    });
    
    it('toContainEqual for object arrays', () => {
        const users = [{id: 1, name: 'John'}, {id: 2, name: 'Jane'}];
        expect(users).toContainEqual({id: 1, name: 'John'});
    });
    
    it('arrayContaining for partial match', () => {
        expect([1, 2, 3, 4]).toEqual(expect.arrayContaining([2, 3]));
    });
});

// Object matchers
describe('Object matchers', () => {
    it('toHaveProperty', () => {
        const user = {name: 'John', age: 30};
        
        expect(user).toHaveProperty('name');
        expect(user).toHaveProperty('name', 'John');
        expect(user).toHaveProperty('age', 30);
    });
    
    it('objectContaining for partial match', () => {
        const user = {id: 1, name: 'John', email: 'john@example.com'};
        
        expect(user).toEqual(expect.objectContaining({
            name: 'John',
            email: 'john@example.com'
        }));
    });
    
    it('toMatchObject for subset', () => {
        const user = {id: 1, name: 'John', age: 30};
        
        expect(user).toMatchObject({name: 'John'});
        expect(user).toMatchObject({name: 'John', age: 30});
    });
});

// Error matchers
describe('Error matchers', () => {
    it('toThrow', () => {
        function throwError() {
            throw new Error('Something went wrong');
        }
        
        expect(throwError).toThrow();
        expect(throwError).toThrow(Error);
        expect(throwError).toThrow('Something went wrong');
        expect(throwError).toThrow(/went wrong/);
    });
    
    it('not.toThrow', () => {
        function noError() {
            return 'ok';
        }
        
        expect(noError).not.toThrow();
    });
});

// Truthiness matchers
describe('Truthiness matchers', () => {
    it('toBeTruthy/toBeFalsy', () => {
        expect(true).toBeTruthy();
        expect(1).toBeTruthy();
        expect('hello').toBeTruthy();
        expect([]).toBeTruthy();
        
        expect(false).toBeFalsy();
        expect(0).toBeFalsy();
        expect('').toBeFalsy();
        expect(null).toBeFalsy();
        expect(undefined).toBeFalsy();
    });
    
    it('toBeDefined/toBeUndefined', () => {
        const obj = {name: 'John'};
        
        expect(obj.name).toBeDefined();
        expect(obj.age).toBeUndefined();
    });
    
    it('toBeNull', () => {
        expect(null).toBeNull();
        expect(undefined).not.toBeNull();
    });
});

// Async matchers
describe('Async matchers', () => {
    it('resolves', async () => {
        const promise = Promise.resolve('success');
        
        await expect(promise).resolves.toBe('success');
    });
    
    it('rejects', async () => {
        const promise = Promise.reject(new Error('failed'));
        
        await expect(promise).rejects.toThrow('failed');
    });
});

// Custom matchers
expect.extend({
    toBeWithinRange(received, floor, ceiling) {
        const pass = received >= floor && received <= ceiling;
        
        if (pass) {
            return {
                message: () => 
                    `expected ${received} not to be within range ${floor} - ${ceiling}`,
                pass: true
            };
        } else {
            return {
                message: () => 
                    `expected ${received} to be within range ${floor} - ${ceiling}`,
                pass: false
            };
        }
    },
    
    toBeValidEmail(received) {
        const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(received);
        
        return {
            message: () => pass
                ? `expected ${received} not to be a valid email`
                : `expected ${received} to be a valid email`,
            pass
        };
    }
});

describe('Custom matchers', () => {
    it('toBeWithinRange', () => {
        expect(5).toBeWithinRange(1, 10);
        expect(15).not.toBeWithinRange(1, 10);
    });
    
    it('toBeValidEmail', () => {
        expect('user@example.com').toBeValidEmail();
        expect('invalid-email').not.toBeValidEmail();
    });
});

// Test data generation with Faker
// import { faker } from '@faker-js/faker';

function generateUser() {
    return {
        id: faker.datatype.uuid(),
        name: faker.name.fullName(),
        email: faker.internet.email(),
        age: faker.datatype.number({min: 18, max: 80}),
        address: {
            street: faker.address.streetAddress(),
            city: faker.address.city(),
            country: faker.address.country()
        }
    };
}

describe('Faker data generation', () => {
    it('generates realistic test data', () => {
        const user = generateUser();
        
        expect(user.id).toBeDefined();
        expect(user.name).toMatch(/\w+ \w+/);
        expect(user.email).toBeValidEmail();
        expect(user.age).toBeGreaterThanOrEqual(18);
        expect(user.age).toBeLessThanOrEqual(80);
    });
    
    it('generates multiple users', () => {
        const users = Array.from({length: 5}, generateUser);
        
        expect(users).toHaveLength(5);
        expect(users[0].id).not.toBe(users[1].id); // Unique IDs
    });
});

// Snapshot testing
describe('Snapshot testing', () => {
    it('matches inline snapshot', () => {
        const user = {id: 1, name: 'John', email: 'john@example.com'};
        
        expect(user).toMatchInlineSnapshot(`
            {
              "email": "john@example.com",
              "id": 1,
              "name": "John",
            }
        `);
    });
    
    it('matches snapshot file', () => {
        const component = renderComponent();
        expect(component).toMatchSnapshot();
    });
});

// Asymmetric matchers
describe('Asymmetric matchers', () => {
    it('expect.anything()', () => {
        expect({id: 1, name: 'John'}).toEqual({
            id: expect.anything(),
            name: 'John'
        });
    });
    
    it('expect.any(constructor)', () => {
        expect({id: 1, createdAt: new Date()}).toEqual({
            id: 1,
            createdAt: expect.any(Date)
        });
    });
    
    it('expect.stringContaining()', () => {
        expect('hello world').toEqual(expect.stringContaining('world'));
    });
    
    it('expect.stringMatching()', () => {
        expect('test@example.com').toEqual(
            expect.stringMatching(/^[^@]+@[^@]+\.[^@]+$/)
        );
    });
});
Key Points: Jest matchers: toBe (===), toEqual (deep), toMatch (regex), toContain (array/string), toThrow (errors). Number matchers: toBeGreaterThan, toBeCloseTo (floats). Object matchers: toHaveProperty, objectContaining, toMatchObject. Async: resolves, rejects. Create custom matchers with expect.extend. Use Faker for test data generation. Snapshot testing for complex objects. Asymmetric matchers: expect.anything(), expect.any(Type).

6. Integration and End-to-End (E2E) Testing

Testing Pyramid

Level Speed Cost Coverage Quantity
Unit Fast Low Small Many (70%)
Integration Medium Medium Medium Some (20%)
E2E Slow High Large Few (10%)

Integration Test Types

Type Tests Example
Component Integration Multiple units together Service + Repository
API Integration HTTP endpoints REST API routes
Database Integration DB operations CRUD operations
External Service Third-party APIs Payment gateway

E2E Testing Tools

Tool Purpose Features
Cypress Modern E2E testing Real browser, time travel debugging
Playwright Cross-browser E2E Multi-browser, parallel, API testing
Puppeteer Chrome automation Headless Chrome, screenshots
WebdriverIO WebDriver protocol Mobile, desktop, cloud services

Example: Integration testing

// Integration test: Service + Repository
class UserRepository {
    constructor(db) {
        this.db = db;
    }
    
    async create(userData) {
        return this.db.users.insert(userData);
    }
    
    async findById(id) {
        return this.db.users.findOne({id});
    }
    
    async update(id, updates) {
        return this.db.users.update({id}, updates);
    }
}

class UserService2 {
    constructor(repository, emailService) {
        this.repository = repository;
        this.emailService = emailService;
    }
    
    async createUser(userData) {
        const user = await this.repository.create(userData);
        await this.emailService.sendWelcomeEmail(user.email);
        return user;
    }
    
    async updateUser(id, updates) {
        const user = await this.repository.findById(id);
        if (!user) {
            throw new Error('User not found');
        }
        return this.repository.update(id, updates);
    }
}

// Integration test
describe('UserService Integration', () => {
    let db;
    let repository;
    let emailService;
    let userService;
    
    beforeAll(async () => {
        // Setup test database
        db = await setupTestDatabase();
    });
    
    afterAll(async () => {
        await db.disconnect();
    });
    
    beforeEach(async () => {
        // Clear database before each test
        await db.users.clear();
        
        // Create real repository
        repository = new UserRepository(db);
        
        // Mock email service
        emailService = {
            sendWelcomeEmail: jest.fn().mockResolvedValue(true)
        };
        
        userService = new UserService2(repository, emailService);
    });
    
    it('should create user and send welcome email', async () => {
        const userData = {
            name: 'John Doe',
            email: 'john@example.com'
        };
        
        const user = await userService.createUser(userData);
        
        // Verify user created in database
        expect(user.id).toBeDefined();
        expect(user.name).toBe('John Doe');
        
        // Verify can retrieve from database
        const retrieved = await repository.findById(user.id);
        expect(retrieved).toEqual(user);
        
        // Verify email sent
        expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith('john@example.com');
    });
    
    it('should update existing user', async () => {
        // Create user first
        const user = await repository.create({
            name: 'John',
            email: 'john@example.com'
        });
        
        // Update user
        const updated = await userService.updateUser(user.id, {
            name: 'John Updated'
        });
        
        expect(updated.name).toBe('John Updated');
        
        // Verify in database
        const retrieved = await repository.findById(user.id);
        expect(retrieved.name).toBe('John Updated');
    });
    
    it('should throw error when updating non-existent user', async () => {
        await expect(
            userService.updateUser(999, {name: 'Test'})
        ).rejects.toThrow('User not found');
    });
});

// API Integration testing with Supertest
// import request from 'supertest';
// import express from 'express';

const app = express();
app.use(express.json());

app.post('/api/users', async (req, res) => {
    const user = await createUser(req.body);
    res.status(201).json(user);
});

app.get('/api/users/:id', async (req, res) => {
    const user = await findUser(req.params.id);
    if (!user) {
        return res.status(404).json({error: 'User not found'});
    }
    res.json(user);
});

describe('User API', () => {
    beforeAll(async () => {
        await setupTestDatabase();
    });
    
    afterEach(async () => {
        await clearDatabase();
    });
    
    describe('POST /api/users', () => {
        it('should create user', async () => {
            const response = await request(app)
                .post('/api/users')
                .send({
                    name: 'John Doe',
                    email: 'john@example.com'
                })
                .expect(201)
                .expect('Content-Type', /json/);
            
            expect(response.body).toMatchObject({
                name: 'John Doe',
                email: 'john@example.com'
            });
            expect(response.body.id).toBeDefined();
        });
        
        it('should return 400 for invalid data', async () => {
            await request(app)
                .post('/api/users')
                .send({name: 'John'}) // Missing email
                .expect(400);
        });
    });
    
    describe('GET /api/users/:id', () => {
        it('should get user by id', async () => {
            // Create user first
            const created = await createUser({
                name: 'Jane',
                email: 'jane@example.com'
            });
            
            const response = await request(app)
                .get(`/api/users/${created.id}`)
                .expect(200);
            
            expect(response.body).toEqual(created);
        });
        
        it('should return 404 for non-existent user', async () => {
            await request(app)
                .get('/api/users/999')
                .expect(404);
        });
    });
});

Example: E2E testing with Playwright

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

test.describe('Shopping Cart E2E', () => {
    test.beforeEach(async ({ page }) => {
        // Navigate to app
        await page.goto('http://localhost:3000');
    });
    
    test('should add item to cart and checkout', async ({ page }) => {
        // Navigate to products
        await page.click('text=Products');
        
        // Wait for products to load
        await page.waitForSelector('.product-card');
        
        // Add first product to cart
        await page.click('.product-card:first-child button:has-text("Add to Cart")');
        
        // Verify cart count updated
        const cartCount = await page.textContent('.cart-count');
        expect(cartCount).toBe('1');
        
        // Go to cart
        await page.click('text=Cart');
        
        // Verify item in cart
        await expect(page.locator('.cart-item')).toHaveCount(1);
        
        // Verify total
        const total = await page.textContent('.cart-total');
        expect(total).toContain("$");
        
        // Proceed to checkout
        await page.click('button:has-text("Checkout")');
        
        // Fill checkout form
        await page.fill('input[name="name"]', 'John Doe');
        await page.fill('input[name="email"]', 'john@example.com');
        await page.fill('input[name="address"]', '123 Main St');
        await page.fill('input[name="cardNumber"]', '4111111111111111');
        
        // Submit order
        await page.click('button:has-text("Place Order")');
        
        // Wait for confirmation
        await page.waitForSelector('.order-confirmation');
        
        // Verify success message
        const message = await page.textContent('.order-confirmation h2');
        expect(message).toContain('Order Confirmed');
        
        // Verify order number
        await expect(page.locator('.order-number')).toBeVisible();
    });
    
    test('should search for products', async ({ page }) => {
        // Enter search term
        await page.fill('input[placeholder="Search products"]', 'laptop');
        await page.press('input[placeholder="Search products"]', 'Enter');
        
        // Wait for results
        await page.waitForSelector('.product-card');
        
        // Verify all results contain search term
        const products = await page.$('.product-card');
        expect(products.length).toBeGreaterThan(0);
        
        for (const product of products) {
            const text = await product.textContent();
            expect(text.toLowerCase()).toContain('laptop');
        }
    });
    
    test('should handle empty cart checkout', async ({ page }) => {
        // Try to checkout with empty cart
        await page.click('text=Cart');
        
        // Checkout button should be disabled
        const checkoutBtn = page.locator('button:has-text("Checkout")');
        await expect(checkoutBtn).toBeDisabled();
        
        // Verify empty cart message
        await expect(page.locator('text=Your cart is empty')).toBeVisible();
    });
});

// Cypress E2E test
// describe('Login Flow', () => {
//     beforeEach(() => {
//         cy.visit('http://localhost:3000/login');
//     });
//     
//     it('should login successfully', () => {
//         // Fill form
//         cy.get('input[name="email"]').type('user@example.com');
//         cy.get('input[name="password"]').type('password123');
//         
//         // Submit
//         cy.get('button[type="submit"]').click();
//         
//         // Verify redirect
//         cy.url().should('include', '/dashboard');
//         
//         // Verify user info displayed
//         cy.contains('Welcome, user@example.com');
//     });
//     
//     it('should show error for invalid credentials', () => {
//         cy.get('input[name="email"]').type('wrong@example.com');
//         cy.get('input[name="password"]').type('wrongpass');
//         cy.get('button[type="submit"]').click();
//         
//         // Verify error message
//         cy.contains('Invalid credentials');
//         
//         // Should stay on login page
//         cy.url().should('include', '/login');
//     });
//     
//     it('should validate form fields', () => {
//         // Try to submit empty form
//         cy.get('button[type="submit"]').click();
//         
//         // Verify validation errors
//         cy.contains('Email is required');
//         cy.contains('Password is required');
//     });
// });

// Visual regression testing
test('should match screenshot', async ({ page }) => {
    await page.goto('http://localhost:3000');
    
    // Take full page screenshot
    await expect(page).toHaveScreenshot('homepage.png');
    
    // Take element screenshot
    const header = page.locator('header');
    await expect(header).toHaveScreenshot('header.png');
});

// Mobile testing
test('should work on mobile', async ({ page }) => {
    // Set mobile viewport
    await page.setViewportSize({ width: 375, height: 667 });
    
    await page.goto('http://localhost:3000');
    
    // Verify mobile menu
    await page.click('.mobile-menu-icon');
    await expect(page.locator('.mobile-menu')).toBeVisible();
});

// Performance testing
test('should load within 3 seconds', async ({ page }) => {
    const startTime = Date.now();
    
    await page.goto('http://localhost:3000');
    await page.waitForLoadState('networkidle');
    
    const loadTime = Date.now() - startTime;
    expect(loadTime).toBeLessThan(3000);
});
Key Points: Integration tests test multiple components together (service + repository, API endpoints). Use real database or test database. E2E tests test complete user workflows in browser. Testing pyramid: many unit tests (70%), some integration (20%), few E2E (10%). Use Supertest for API testing. Playwright/Cypress for browser automation. E2E tests are slow, expensive - focus on critical paths. Include visual regression, mobile, performance testing.

Section 24 Summary: Testing and Code Quality

  • Unit Testing: Test individual units in isolation, follow AAA pattern (Arrange, Act, Assert), fast and repeatable
  • Test Structure: Organize with describe/it blocks, setup with beforeEach/afterEach, descriptive test names
  • Mocking: Use mocks (verify interactions), stubs (fixed responses), spies (track calls), jest.fn() for mocks
  • Mock Assertions: toHaveBeenCalled, toHaveBeenCalledWith, toHaveBeenCalledTimes, mockReturnValue, mockResolvedValue
  • TDD Cycle: Red (write failing test), Green (make it pass), Refactor (improve code while tests pass)
  • TDD Benefits: Better design, documentation, confidence, regression prevention, focus on requirements
  • Code Coverage: Measure line, branch, function, statement coverage - aim for 80-90%, branch coverage most thorough
  • Quality Metrics: Cyclomatic complexity, maintainability index, code duplication, technical debt
  • Assertions: toBe (===), toEqual (deep), toMatch (regex), toContain, toThrow, toBeGreaterThan, toHaveProperty
  • Test Utilities: Faker for test data, custom matchers with expect.extend, snapshot testing, asymmetric matchers
  • Integration Tests: Test multiple components together (service + repository, API endpoints), use test database
  • E2E Testing: Test complete workflows with Playwright/Cypress, testing pyramid (70% unit, 20% integration, 10% E2E)