|$ curl https://forge-ai.dev/api/markdown?path=docs/js/testing
$cat docs/javascript-—-testing-&-debugging.md
updated This week·30 min read·published

JavaScript — Testing & Debugging

JavaScriptTestingDebuggingIntermediateIntermediate
Introduction

Testing and debugging are fundamental to writing reliable JavaScript applications. Testing provides a safety net for refactoring, documents expected behavior, and catches regressions before they reach production. Debugging is the skill of systematically identifying and resolving issues when they arise. Together, they form the quality assurance foundation of professional JavaScript development.

The JavaScript testing ecosystem has matured significantly. Modern testing frameworks (Vitest, Jest, Playwright) provide fast, reliable test runners with excellent developer experience. The testing pyramid — unit tests at the base, integration tests in the middle, end-to-end tests at the top — guides investment across different test types. Each level provides different confidence at different costs.

Debugging has also evolved beyond console.log. Browser DevTools offer sophisticated debuggers, performance profilers, memory inspectors, and network analyzers. Node.js provides built-in debugging via the --inspect flag. The key is knowing which tool to use for which class of problem.

testing-intro.js
JavaScript
1// The testing mindset — write tests that fail before they pass
2// Red-Green-Refactor cycle:
3// 1. RED: Write a failing test that describes the desired behavior
4// 2. GREEN: Write the minimum code to make it pass
5// 3. REFACTOR: Improve the code while keeping tests green
6
7// Example: TDD approach
8import { describe, it, expect } from 'vitest';
9
10// RED phase — test defines the API first
11describe('add()', () => {
12 it('should add two positive numbers', () => {
13 const result = add(2, 3);
14 expect(result).toBe(5);
15 });
16
17 it('should handle negative numbers', () => {
18 expect(add(-1, -2)).toBe(-3);
19 });
20
21 it('should throw on non-number input', () => {
22 expect(() => add('a', 1)).toThrow('Invalid input');
23 });
24});
25
26// GREEN phase — implement to pass tests
27function add(a, b) {
28 if (typeof a !== 'number' || typeof b !== 'number') {
29 throw new Error('Invalid input');
30 }
31 return a + b;
32}
Unit Testing

Unit tests verify the smallest testable parts of an application in isolation — individual functions, methods, or modules. They are fast, deterministic, and provide precise failure localization. A good unit test covers the happy path, edge cases, error conditions, and boundary values. Mocking isolates the unit under test from its dependencies.

FrameworkRuntimeFeatures
VitestNode/ViteFast, Jest-compatible API, ESM native, TypeScript built-in
JestNodeMature, large ecosystem, built-in mocking, snapshot testing
MochaNodeFlexible, minimal, requires assertion library (Chai, expect)
Node:testNode 20+Built-in test runner (assert, mock, describe/it)
unit-testing.js
JavaScript
1// Vitest — modern unit testing (Jest-compatible)
2import { describe, it, expect, vi, beforeEach } from 'vitest';
3
4import { fetchUserData, processOrder, calculateDiscount } from './services';
5
6// Mocking external dependencies
7vi.mock('./api', () => ({
8 get: vi.fn(),
9 post: vi.fn(),
10}));
11
12import { api } from './api';
13
14beforeEach(() => {
15 vi.clearAllMocks();
16});
17
18describe('fetchUserData', () => {
19 it('should return formatted user data on success', async () => {
20 vi.mocked(api.get).mockResolvedValue({
21 data: { id: 1, name: 'Alice', email: 'alice@example.com' },
22 });
23
24 const result = await fetchUserData(1);
25
26 expect(result).toEqual({
27 id: 1,
28 name: 'Alice',
29 email: 'alice@example.com',
30 });
31 expect(api.get).toHaveBeenCalledWith('/users/1');
32 });
33
34 it('should throw when user is not found', async () => {
35 vi.mocked(api.get).mockRejectedValue(new Error('Not found'));
36
37 await expect(fetchUserData(999)).rejects.toThrow('User not found');
38 });
39});
40
41describe('calculateDiscount', () => {
42 it('should apply 10% discount for orders over $100', () => {
43 expect(calculateDiscount(150)).toBe(135);
44 });
45
46 it('should apply 20% discount for VIP customers', () => {
47 expect(calculateDiscount(50, { isVip: true })).toBe(40);
48 });
49
50 it('should not apply discount for orders under $20', () => {
51 expect(calculateDiscount(15)).toBe(15);
52 });
53
54 it('should throw on negative amounts', () => {
55 expect(() => calculateDiscount(-10)).toThrow('Invalid amount');
56 });
57});
58
59// Snapshot testing — verify output hasn't changed
60it('should match the order summary snapshot', () => {
61 const order = processOrder({ items: ['apple', 'banana'], total: 25 });
62 expect(order).toMatchSnapshot();
63});
64
65// Testing async operations with timers
66vi.useFakeTimers();
67
68it('should debounce the search call', () => {
69 const searchSpy = vi.fn();
70 const debouncedSearch = vi.fn(debounce(searchSpy, 300));
71
72 debouncedSearch('a');
73 debouncedSearch('ap');
74 debouncedSearch('app');
75
76 vi.advanceTimersByTime(300);
77
78 expect(searchSpy).toHaveBeenCalledTimes(1);
79 expect(searchSpy).toHaveBeenCalledWith('app');
80});

info

Write tests that test behavior, not implementation. A test that asserts internal implementation details (specific function calls, private methods) breaks when the implementation changes even if the behavior is correct. Test the public API: what goes in and what comes out. This gives you the freedom to refactor without rewriting tests.
Integration Testing

Integration tests verify that different parts of the application work together correctly. Unlike unit tests, integration tests exercise real interactions between modules, databases, APIs, and external services. They catch issues that unit tests miss — incorrect API contracts, serialization mismatches, authentication flows, and database query errors.

integration-testing.js
JavaScript
1// API integration testing with supertest
2import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3import request from 'supertest';
4import { createApp } from './app';
5import { db } from './database';
6
7const app = createApp();
8
9beforeAll(async () => {
10 await db.migrate.latest();
11 await db.seed.run();
12});
13
14afterAll(async () => {
15 await db.destroy();
16});
17
18describe('POST /api/users', () => {
19 it('should create a new user', async () => {
20 const res = await request(app)
21 .post('/api/users')
22 .send({ name: 'Alice', email: 'alice@test.com' })
23 .expect(201);
24
25 expect(res.body).toMatchObject({
26 name: 'Alice',
27 email: 'alice@test.com',
28 });
29 expect(res.body).toHaveProperty('id');
30 expect(res.body).toHaveProperty('createdAt');
31 });
32
33 it('should reject duplicate emails', async () => {
34 await request(app)
35 .post('/api/users')
36 .send({ name: 'Alice', email: 'alice@test.com' })
37 .expect(409);
38 });
39
40 it('should validate required fields', async () => {
41 const res = await request(app)
42 .post('/api/users')
43 .send({ name: 'Alice' })
44 .expect(400);
45
46 expect(res.body.errors).toContainEqual(
47 expect.objectContaining({ field: 'email' })
48 );
49 });
50});
51
52// Database integration testing
53describe('UserRepository', () => {
54 it('should persist and retrieve user data', async () => {
55 const user = await UserRepository.create({
56 name: 'Bob',
57 email: 'bob@test.com',
58 });
59
60 const found = await UserRepository.findById(user.id);
61 expect(found).toEqual(user);
62 });
63
64 it('should handle concurrent updates with versioning', async () => {
65 const user = await UserRepository.create({ name: 'Charlie' });
66
67 const [update1, update2] = await Promise.all([
68 UserRepository.updateName(user.id, 'Charles'),
69 UserRepository.updateName(user.id, 'Chuck'),
70 ]);
71
72 // One update succeeds, the other fails with version conflict
73 expect(update1).not.toEqual(update2);
74 });
75});
76
77// Component integration testing with React Testing Library
78import { render, screen, waitFor } from '@testing-library/react';
79import userEvent from '@testing-library/user-event';
80
81it('should submit the form and show success message', async () => {
82 render(<ContactForm />);
83
84 await userEvent.type(screen.getByLabelText('Name'), 'Alice');
85 await userEvent.type(screen.getByLabelText('Email'), 'alice@test.com');
86 await userEvent.click(screen.getByRole('button', { name: 'Submit' }));
87
88 await waitFor(() => {
89 expect(screen.getByText('Message sent!')).toBeInTheDocument();
90 });
91});

best practice

Integration tests should use a real (or in-memory) database, not mocked. Use test containers or SQLite for local development. Always clean the database between test runs to avoid state leakage. Focus on testing critical user flows — registration, authentication, payment processing — rather than every possible input combination (that is unit test territory).
Testing Frameworks

Choosing the right testing framework depends on your project type, performance requirements, and team preferences. Vitest has emerged as the leading choice for Vite-based projects, offering near-instant HMR-powered test execution. Playwright dominates end-to-end testing with cross-browser support. The landscape continues to evolve toward faster, simpler tooling.

CategoryFrameworkBest For
UnitVitestFast unit/integration tests with Vite ecosystem
UnitJestLarge codebases, established CI pipelines
ComponentTesting LibraryUser-centric React/Vue/Angular component tests
E2EPlaywrightCross-browser E2E, mobile emulation, network interception
E2ECypressDeveloper-friendly E2E with time-travel debugging
APISuperTestHTTP integration testing without running a server
Coveragec8 / IstanbulCode coverage reports (line, branch, function, statement)
preview
Debugging Techniques

Debugging is the systematic process of identifying the root cause of unexpected behavior. The most effective debugging follows the scientific method: form a hypothesis, predict an outcome, test it with an experiment, and refine based on results. Modern browser DevTools provide powerful debugging capabilities beyond console.log — conditional breakpoints, logpoints, watch expressions, and call stack inspection.

debugging-techniques.js
JavaScript
1// Console-based debugging — beyond console.log
2console.log('Simple message');
3console.error('Error:', err);
4console.warn('Warning:', warning);
5console.info('Info message');
6console.debug('Debug detail:', detail);
7
8// Object inspection
9console.table(usersArray); // Table format for arrays
10console.dir(element); // DOM element properties
11console.dirxml(document.body); // HTML/XML representation
12
13// Timing and profiling
14console.time('operation');
15heavyTask();
16console.timeEnd('operation'); // "operation: 145.2ms"
17
18console.timeLog('operation', 'step 1 complete');
19// Logs time elapsed since the last time/timeLog call
20
21// Stack traces
22console.trace('Trace point');
23// Prints a stack trace to the console
24
25// Grouping
26console.group('User Details');
27console.log('Name:', name);
28console.log('Age:', age);
29console.groupEnd();
30
31// Conditional breakpoints — in DevTools, set breakpoints with conditions:
32// e.g., count > 5 && type === 'error'
33
34// Logpoints — DevTools feature: log to console without pausing
35// Right-click line number → "Add logpoint" → type the expression
36
37// Debugger statement — programmatic breakpoint
38function suspiciousFunction(value) {
39 debugger; // execution pauses here if DevTools is open
40 return value * 2;
41}
42
43// Source maps — debug compiled code in original form
44// Enable in build config:
45// webpack: devtool: 'source-map'
46// vite: build.sourcemap: true
47// TypeScript: sourceMap: true
48
49// Async debugging — async call stacks
50// In Chrome DevTools, enable "Async" in Sources panel
51// Shows the full call chain across async boundaries
52
53// Network debugging
54// Use console.assert for conditional logging
55console.assert(user.age > 0, 'Invalid age:', user);
56// Only logs if the assertion fails
🔥

pro tip

The debugger statement is your most powerful debugging tool. Combined with conditional breakpoints, you can pause execution exactly when state reaches an unexpected value. In Chrome DevTools, use "Blackbox" to skip library code and focus on your application. For Node.js, use node --inspect-brk to debug server-side code with the same DevTools interface.
DevTools

Browser DevTools are the most important tool in a JavaScript developer's arsenal. Chrome DevTools, Firefox Developer Tools, and Safari Web Inspector each provide a comprehensive suite for debugging, profiling, and inspecting web applications. Mastering these tools separates novice developers from experienced professionals.

PanelPurposeKey Feature
ElementsInspect and modify DOM/CSSLive CSS editing, box model visualization, accessibility tree
ConsoleJavaScript REPL and logging$0 for selected element, console.table, live expressions
SourcesFull-featured debuggerBreakpoints, step-through, call stack, scope variables
NetworkNetwork request inspectionWaterfall timing, request/response headers, blocking, throttling
PerformanceRuntime performance profilingFlame charts, FPS meter, long task detection
MemoryHeap inspection and profilingHeap snapshots, allocation timeline, detached DOM tree finder
ApplicationStorage, Service Workers, CachesIndexedDB inspector, cache storage, manifest viewer
LighthouseAudit and scorePerformance, accessibility, SEO, best practices scoring
devtools-tips.js
JavaScript
1// Console utilities in DevTools
2// $0 — last selected DOM element in Elements panel
3$0; // the element you last inspected
4$0.style.backgroundColor = '#00FF41'; // modify it live
5
6// $_ — last evaluated expression result
72 + 2; // 4
8$_ + 3; // 7 (uses previous result)
9
10// $() — document.querySelector shorthand
11$('button.submit'); // first matching element
12$$('li.item'); // all matching elements (querySelectorAll)
13
14// $x() — XPath queries
15$x('//div[@class="main"]');
16
17// monitor() / unmonitor() — log function calls
18monitor(fetch);
19// Every call to fetch() will log to console
20
21// copy() — copy to clipboard
22copy(JSON.stringify(data, null, 2));
23
24// Performance marks in DevTools
25performance.mark('render-start');
26// Record in Performance panel timeline — visible in waterfall
27
28// Live Expressions — pin expressions to console header
29// Click the eye icon in Console, type: document.activeElement
30// Updates in real-time as you interact with the page
31
32// Coverage tab — find unused JavaScript/CSS
33// Coverage tab → Record → interact with the page
34// Red = unused code, Green = used code
35// Helps identify dead code to remove
36
37// Rendering tab — visual debugging
38// Show paint rectangles (flashing green = repaint regions)
39// FPS meter (real-time frame rate)
40// Layout shift regions (debug CLS issues)
41
42// Breakpoint types in Sources panel:
43// Line breakpoint — pause at a specific line
44// Conditional breakpoint — pause when expression is true
45// DOM breakpoint — pause on subtree modification, attribute change, node removal
46// XHR/fetch breakpoint — pause when URL contains a string
47// Event listener breakpoint — pause on specific event types

best practice

The most underused DevTools feature: conditional breakpoints. Instead of if (bug) { debugger; } in your code, right-click the line number in Sources and add a condition. This avoids modifying source code and works in production (as long as source maps are available). Second most underused: Coverage tab — identify exactly which code executes during a user flow and remove the rest.
Common Patterns

Certain testing patterns emerge repeatedly across JavaScript projects. These patterns solve common problems: testing asynchronous code, handling time-dependent logic, mocking network requests, and organizing tests for maintainability. Mastering these patterns makes test suites more reliable and easier to maintain.

testing-patterns.js
JavaScript
1// Pattern 1: Factory functions for test data
2function buildUser(overrides = {}) {
3 return {
4 id: crypto.randomUUID(),
5 name: 'Default User',
6 email: 'user@example.com',
7 role: 'user',
8 createdAt: new Date('2025-01-01'),
9 ...overrides,
10 };
11}
12
13it('should allow admin to delete users', () => {
14 const admin = buildUser({ role: 'admin' });
15 const regular = buildUser({ role: 'user' });
16 // Test with controlled, predictable data
17});
18
19// Pattern 2: Testing async error handling
20it('should handle API errors gracefully', async () => {
21 vi.mocked(api.fetch).mockRejectedValue(
22 new HTTPError(500, 'Internal Server Error')
23 );
24
25 const result = await safeFetch('/data');
26
27 expect(result).toEqual({
28 error: true,
29 message: 'Server error. Please try again later.',
30 retryAfter: 5000,
31 });
32});
33
34// Pattern 3: Time-dependent logic (useFakeTimers)
35vi.useFakeTimers();
36
37it('should retry after configured delay', async () => {
38 const retrySpy = vi.fn();
39 const sut = createRetryHandler(retrySpy, { delay: 1000 });
40
41 sut.execute();
42 expect(retrySpy).toHaveBeenCalledTimes(1);
43
44 vi.advanceTimersByTime(999);
45 expect(retrySpy).toHaveBeenCalledTimes(1); // not yet
46
47 vi.advanceTimersByTime(1);
48 expect(retrySpy).toHaveBeenCalledTimes(2); // retried
49});
50
51// Pattern 4: Testing with localStorage
52beforeEach(() => {
53 localStorage.clear();
54});
55
56it('should persist user preferences', () => {
57 const prefs = { theme: 'dark', fontSize: 14 };
58 savePreferences(prefs);
59
60 const loaded = loadPreferences();
61 expect(loaded).toEqual(prefs);
62});
63
64// Pattern 5: Testing random values (seed-based)
65it('should generate unique IDs', () => {
66 const ids = new Set();
67 for (let i = 0; i < 1000; i++) {
68 ids.add(generateId());
69 }
70 expect(ids.size).toBe(1000);
71});
72
73// Pattern 6: Parametrized tests
74it.each([
75 { input: 0, expected: false },
76 { input: 1, expected: false },
77 { input: 2, expected: true },
78 { input: 3, expected: true },
79 { input: 17, expected: true },
80])('should return $expected for isPrime($input)', ({ input, expected }) => {
81 expect(isPrime(input)).toBe(expected);
82});
83
84// Pattern 7: Snapshot for complex objects
85it('should generate correct invoice PDF structure', () => {
86 const invoice = generateInvoice(invoiceData);
87 expect(invoice).toMatchSnapshot({
88 id: expect.any(String),
89 generatedAt: expect.any(Number),
90 });
91});
Best Practices
Write tests before code (TDD) for complex logic — it produces better APIs and higher coverage
Follow the AAA pattern: Arrange (set up), Act (execute), Assert (verify)
Test one concept per test — if a test fails, you should know exactly what broke
Avoid testing implementation details — test public behavior and contracts
Use factories and builders for test data instead of inline object literals
Mock at the boundary of your system — not internal modules you don't own
Keep tests fast — if they take too long, developers stop running them
Use code coverage to find untested code, not as a target (100% is rarely practical)
Write integration tests for critical user journeys — they catch real integration bugs
Debug with the scientific method: hypothesize, predict, experiment, refine
🔥

pro tip

The single most impactful change you can make to your testing workflow: run tests on every file save. Vitest and Jest both support watch mode. Combined with fast test runners, this gives sub-second feedback loops. When a test fails, the fix is usually still in your working memory. This is significantly more effective than running tests minutes later during a CI pipeline.
$Blueprint — Engineering Documentation·Section ID: JS-TESTING·Revision: 1.0