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
