Chapter 16: Testing and Debugging
· 11 min read
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