Back to Articles

Chapter 16: Testing and Debugging

July 27, 202511 min read
javascripttestingdebuggingunit-testsmockingconsoledebugging-tools
Chapter 16: Testing and Debugging

Chapter 16: Testing and Debugging

Debugging Techniques

// Console methods beyond console.log
const user = { name: "Alice", age: 30, hobbies: ["reading", "coding"] };

console.table(user); // Display as table
console.group("User Details");
console.log("Name:", user.name);
console.log("Age:", user.age);
console.groupEnd();

console.time("Performance Test");
// Some code to measure
for (let i = 0; i < 1000000; i++) {
    // Do something
}
console.timeEnd("Performance Test");

console.assert(user.age > 18, "User must be an adult");
console.count("Function calls"); // Count how many times this runs

// Debugging with breakpoints in code
function complexFunction(data) {
    debugger; // Execution will pause here when dev tools are open
    
    const processed = data.map(item => {
        // Processing logic
        return item * 2;
    });
    
    return processed;
}

// Stack trace
function a() { b(); }
function b() { c(); }
function c() { console.trace(); }
a(); // Shows the call stack

Unit Testing Examples

// Simple test framework (like Jest/Mocha)
function describe(description, tests) {
    console.log(`\n${description}`);
    tests();
}

function it(description, test) {
    try {
        test();
        console.log(`✅ ${description}`);
    } catch (error) {
        console.log(`❌ ${description}`);
        console.error(error.message);
    }
}

function expect(actual) {
    return {
        toBe(expected) {
            if (actual !== expected) {
                throw new Error(`Expected ${expected}, but got ${actual}`);
            }
        },
        toEqual(expected) {
            if (JSON.stringify(actual) !== JSON.stringify(expected)) {
                throw new Error(`Expected ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`);
            }
        },
        toThrow() {
            if (typeof actual !== 'function') {
                throw new Error('Expected a function');
            }
            try {
                actual();
                throw new Error('Expected function to throw');
            } catch (error) {
                // Function threw as expected
            }
        }
    };
}

// Test examples
describe("Calculator Tests", () => {
    const calculator = {
        add: (a, b) => a + b,
        subtract: (a, b) => a - b,
        divide: (a, b) => {
            if (b === 0) throw new Error("Division by zero");
            return a / b;
        }
    };
    
    it("should add two numbers", () => {
        expect(calculator.add(2, 3)).toBe(5);
    });
    
    it("should subtract two numbers", () => {
        expect(calculator.subtract(5, 3)).toBe(2);
    });
    
    it("should throw error when dividing by zero", () => {
        expect(() => calculator.divide(5, 0)).toThrow();
    });
});

// Mocking and stubbing
function createMock() {
    const calls = [];
    const mock = function(...args) {
        calls.push(args);
        return mock.returnValue;
    };
    
    mock.calls = calls;
    mock.returnValue = undefined;
    mock.returns = function(value) {
        this.returnValue = value;
        return this;
    };
    
    return mock;
}

// Usage
const mockCallback = createMock().returns("mocked result");
const result = mockCallback("arg1", "arg2");
console.log(result); // "mocked result"
console.log(mockCallback.calls); // [["arg1", "arg2"]]

Advanced Debugging Techniques

Error Boundary Pattern

class ErrorBoundary {
    constructor() {
        this.errors = [];
        this.setupGlobalHandlers();
    }
    
    setupGlobalHandlers() {
        // Catch synchronous errors
        window.addEventListener('error', (event) => {
            this.handleError({
                message: event.message,
                filename: event.filename,
                lineno: event.lineno,
                colno: event.colno,
                error: event.error,
                type: 'javascript'
            });
        });
        
        // Catch unhandled promise rejections
        window.addEventListener('unhandledrejection', (event) => {
            this.handleError({
                message: event.reason?.message || 'Unhandled promise rejection',
                error: event.reason,
                type: 'promise'
            });
        });
    }
    
    handleError(errorInfo) {
        console.error('Error caught by boundary:', errorInfo);
        this.errors.push({
            ...errorInfo,
            timestamp: new Date().toISOString(),
            userAgent: navigator.userAgent,
            url: window.location.href
        });
        
        // Send to logging service
        this.reportError(errorInfo);
    }
    
    reportError(errorInfo) {
        // Mock error reporting service
        fetch('/api/errors', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(errorInfo)
        }).catch(err => {
            console.error('Failed to report error:', err);
        });
    }
    
    getErrors() {
        return [...this.errors];
    }
    
    clearErrors() {
        this.errors = [];
    }
}

// Initialize error boundary
const errorBoundary = new ErrorBoundary();

Debug Utilities

class DebugUtils {
    static createLogger(namespace) {
        const isDebugEnabled = localStorage.getItem('debug')?.includes(namespace) || 
                             process?.env?.NODE_ENV === 'development';
        
        return {
            log: (...args) => {
                if (isDebugEnabled) {
                    console.log(`[${namespace}]`, ...args);
                }
            },
            warn: (...args) => {
                if (isDebugEnabled) {
                    console.warn(`[${namespace}]`, ...args);
                }
            },
            error: (...args) => {
                console.error(`[${namespace}]`, ...args);
            },
            group: (label) => {
                if (isDebugEnabled) {
                    console.group(`[${namespace}] ${label}`);
                }
            },
            groupEnd: () => {
                if (isDebugEnabled) {
                    console.groupEnd();
                }
            }
        };
    }
    
    static deepClone(obj) {
        // For debugging - proper deep clone without references
        return JSON.parse(JSON.stringify(obj));
    }
    
    static objectDiff(obj1, obj2) {
        const diff = {};
        const keys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
        
        for (const key of keys) {
            if (obj1[key] !== obj2[key]) {
                diff[key] = {
                    old: obj1[key],
                    new: obj2[key]
                };
            }
        }
        
        return diff;
    }
    
    static measureMemory() {
        if (performance.memory) {
            return {
                used: `${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
                total: `${(performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
                limit: `${(performance.memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)} MB`
            };
        }
        return 'Memory API not available';
    }
    
    static traceCallStack(skipFrames = 0) {
        const stack = new Error().stack;
        return stack
            .split('\n')
            .slice(2 + skipFrames) // Skip Error() and this function
            .map(line => line.trim())
            .join('\n');
    }
}

// Usage
const logger = DebugUtils.createLogger('MyApp');
logger.log('Application started');

// Enable debugging in console: localStorage.setItem('debug', 'MyApp');

Testing Patterns and Best Practices

Test Data Builders

class UserBuilder {
    constructor() {
        this.user = {
            id: 1,
            name: 'John Doe',
            email: 'john@example.com',
            age: 30,
            active: true,
            roles: ['user']
        };
    }
    
    withId(id) {
        this.user.id = id;
        return this;
    }
    
    withName(name) {
        this.user.name = name;
        return this;
    }
    
    withEmail(email) {
        this.user.email = email;
        return this;
    }
    
    withAge(age) {
        this.user.age = age;
        return this;
    }
    
    inactive() {
        this.user.active = false;
        return this;
    }
    
    withRoles(...roles) {
        this.user.roles = roles;
        return this;
    }
    
    build() {
        return { ...this.user };
    }
}

// Usage in tests
describe("User Service Tests", () => {
    it("should validate adult users", () => {
        const adultUser = new UserBuilder()
            .withAge(25)
            .build();
        
        const minorUser = new UserBuilder()
            .withAge(16)
            .build();
        
        expect(UserService.isAdult(adultUser)).toBe(true);
        expect(UserService.isAdult(minorUser)).toBe(false);
    });
    
    it("should filter active admin users", () => {
        const users = [
            new UserBuilder().withRoles('admin').build(),
            new UserBuilder().withRoles('admin').inactive().build(),
            new UserBuilder().withRoles('user').build()
        ];
        
        const activeAdmins = UserService.getActiveAdmins(users);
        expect(activeAdmins).toHaveLength(1);
    });
});

Async Testing Utilities

class AsyncTestUtils {
    static async waitFor(conditionFn, timeout = 5000, interval = 100) {
        const startTime = Date.now();
        
        while (Date.now() - startTime < timeout) {
            if (await conditionFn()) {
                return;
            }
            await this.delay(interval);
        }
        
        throw new Error(`Condition not met within ${timeout}ms`);
    }
    
    static delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    static async expectToThrowAsync(asyncFn, expectedError) {
        try {
            await asyncFn();
            throw new Error('Expected function to throw');
        } catch (error) {
            if (expectedError && !error.message.includes(expectedError)) {
                throw new Error(`Expected error containing "${expectedError}", got "${error.message}"`);
            }
        }
    }
    
    static createMockPromise() {
        let resolve, reject;
        const promise = new Promise((res, rej) => {
            resolve = res;
            reject = rej;
        });
        
        return { promise, resolve, reject };
    }
}

// Usage
describe("Async Function Tests", () => {
    it("should handle API timeouts", async () => {
        const { promise, reject } = AsyncTestUtils.createMockPromise();
        
        // Mock API that times out
        const mockApi = () => promise;
        
        // Simulate timeout after 100ms
        setTimeout(() => reject(new Error('Timeout')), 100);
        
        await AsyncTestUtils.expectToThrowAsync(
            () => mockApi(),
            'Timeout'
        );
    });
    
    it("should wait for DOM element to appear", async () => {
        // Simulate element appearing after delay
        setTimeout(() => {
            const element = document.createElement('div');
            element.id = 'test-element';
            document.body.appendChild(element);
        }, 200);
        
        await AsyncTestUtils.waitFor(
            () => document.getElementById('test-element') !== null
        );
        
        expect(document.getElementById('test-element')).toBeTruthy();
    });
});

Performance Testing

class PerformanceTester {
    static async benchmarkFunction(fn, iterations = 1000) {
        const times = [];
        
        // Warmup
        for (let i = 0; i < 10; i++) {
            await fn();
        }
        
        // Actual benchmarking
        for (let i = 0; i < iterations; i++) {
            const start = performance.now();
            await fn();
            const end = performance.now();
            times.push(end - start);
        }
        
        times.sort((a, b) => a - b);
        
        return {
            min: times[0],
            max: times[times.length - 1],
            mean: times.reduce((a, b) => a + b) / times.length,
            median: times[Math.floor(times.length / 2)],
            p95: times[Math.floor(times.length * 0.95)],
            p99: times[Math.floor(times.length * 0.99)]
        };
    }
    
    static memoryUsageTest(fn) {
        if (!performance.memory) {
            return 'Memory API not available';
        }
        
        const beforeMemory = performance.memory.usedJSHeapSize;
        fn();
        
        // Force garbage collection if available
        if (window.gc) {
            window.gc();
        }
        
        const afterMemory = performance.memory.usedJSHeapSize;
        const difference = afterMemory - beforeMemory;
        
        return {
            before: `${(beforeMemory / 1024 / 1024).toFixed(2)} MB`,
            after: `${(afterMemory / 1024 / 1024).toFixed(2)} MB`,
            difference: `${(difference / 1024 / 1024).toFixed(2)} MB`
        };
    }
}

// Usage
describe("Performance Tests", () => {
    it("should benchmark array operations", async () => {
        const largeArray = Array.from({ length: 100000 }, (_, i) => i);
        
        const mapBenchmark = await PerformanceTester.benchmarkFunction(
            () => largeArray.map(x => x * 2),
            100
        );
        
        console.log('Map operation stats:', mapBenchmark);
        expect(mapBenchmark.mean).toBeLessThan(10); // Should be under 10ms
    });
    
    it("should test memory usage", () => {
        const memoryStats = PerformanceTester.memoryUsageTest(() => {
            const bigArray = new Array(1000000).fill('test');
            // Do something with bigArray
            return bigArray.length;
        });
        
        console.log('Memory usage:', memoryStats);
    });
});

Next Chapter: Chapter 17: Security Best Practices

Previous Chapter: Chapter 15: Performance and Best Practices

Table of Contents: JavaScript Guide