Loading...
Loading...
Write tests that catch bugs before users do. Confidence in every deploy.
Module 10 · Browser Internals & Best Practices
Production JavaScript requires more than syntax. The event loop, rendering pipeline, bundlers, debugging techniques, performance optimization, design patterns, testing, and security practices separate hobby scripts from professional software.
Learn Testing in JavaScript — covering Why Test, Unit Tests, Mocking, Async Testing with interactive visual examples and step-by-step explanations.
Writing JavaScript that works in a tutorial is different from writing JavaScript that ships to production. The event loop explains why setTimeout(0) does not run immediately. Performance profiling finds bottlenecks before users do.
This module covers the internals and best practices that separate hobby scripts from professional software: debugging, bundlers, design patterns, testing strategies, and security fundamentals every developer should know.
Understanding testing is essential for every JavaScript developer. It shows up in frontend UI code, backend APIs, and framework internals — and getting it wrong leads to bugs that are hard to trace. This chapter builds intuition with visuals so the behavior sticks.
Tests are proof that your code works — now and after every change.
// Without tests: // "I changed the login function... did I break anything?" // "This worked last week... what changed?" // "Ship it and pray" 🙏 // With tests: // ✓ Change code → run tests → instant feedback // ✓ Refactor fearlessly — tests catch regressions // ✓ Document behavior — tests show how code should work // ✓ Deploy with confidence // Testing tools landscape: // Test runners: Vitest (fast, ESM), Jest (mature, widespread) // Assertions: built-in expect() in both // DOM testing: Testing Library (@testing-library/*) // E2E: Playwright, Cypress // Coverage: c8, istanbul // This chapter focuses on Vitest (same API as Jest, but faster): // npm install -D vitest
Test one function, one behavior, in isolation.
// The function to test: function add(a, b) { return a + b; } function divide(a, b) { if (b === 0) throw new Error("Cannot divide by zero"); return a / b; } // The test file (add.test.js): import { describe, it, expect } from "vitest"; describe("add", () => { it("adds two positive numbers", () => { expect(add(2, 3)).toBe(5); }); it("handles negative numbers", () => { expect(add(-1, -2)).toBe(-3); }); it("handles zero", () => { expect(add(0, 5)).toBe(5); }); });
Tests follow AAA: Arrange (setup), Act (call function), Assert (check result). Each test checks ONE behavior.
Replace dependencies with controlled fakes.
import { vi, describe, it, expect } from "vitest"; // Mock a function: const mockFetch = vi.fn(); // Control what mock returns: mockFetch.mockResolvedValue({ ok: true, json: () => ({ id: 1 }) }); // Use in test: describe("getUser", () => { it("fetches user by id", async () => { global.fetch = mockFetch; const user = await getUser(1); // Assert it was called correctly: expect(mockFetch).toHaveBeenCalledWith("/api/users/1"); expect(mockFetch).toHaveBeenCalledTimes(1); expect(user).toEqual({ id: 1 }); }); }); // Mock a module: vi.mock("./database", () => ({ query: vi.fn().mockResolvedValue([{ id: 1, name: "Alice" }]), })); // Spy on existing methods (without replacing): const spy = vi.spyOn(console, "log"); doSomething(); expect(spy).toHaveBeenCalledWith("expected output"); spy.mockRestore(); // clean up // Timer mocks: vi.useFakeTimers(); setTimeout(callback, 1000); vi.advanceTimersByTime(1000); expect(callback).toHaveBeenCalled(); vi.useRealTimers();
Test Promises, async/await, and callbacks.
// Async functions — just use async/await in tests: describe("fetchPosts", () => { it("returns posts for a user", async () => { const posts = await fetchPosts(1); expect(posts).toHaveLength(3); expect(posts[0]).toHaveProperty("title"); }); it("throws on invalid user", async () => { await expect(fetchPosts(-1)).rejects.toThrow("User not found"); }); }); // Testing Promise resolution/rejection: it("resolves with data", async () => { await expect(asyncFn()).resolves.toBe("success"); }); it("rejects with error", async () => { await expect(asyncFn()).rejects.toThrow(); }); // Testing event-based code: it("emits after delay", async () => { const result = await new Promise((resolve) => { emitter.on("done", resolve); emitter.start(); }); expect(result).toBe("complete"); }); // Setup and teardown: describe("database tests", () => { beforeEach(async () => { await db.seed(); // fresh data each test }); afterEach(async () => { await db.cleanup(); }); it("inserts a record", async () => { await db.insert({ name: "test" }); const records = await db.findAll(); expect(records).toHaveLength(1); }); });
Test-Driven Development: Red → Green → Refactor.
// TDD Cycle: // 1. RED — Write a failing test first // 2. GREEN — Write minimum code to pass // 3. REFACTOR — Improve code, tests still pass // Example: Build a password validator // Step 1 — RED: Write the test first describe("validatePassword", () => { it("rejects passwords shorter than 8 chars", () => { expect(validatePassword("short")).toBe(false); }); }); // Run test → FAILS (function doesn't exist yet) // ✗ ReferenceError: validatePassword is not defined
RED: Write a test that describes the behavior you want. It fails because the code doesn't exist yet.
Common questions about testing.