Testing
Write tests that catch bugs before users do. Confidence in every deploy.
Why Test
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
Unit Tests
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.
1 / 2
Mocking
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();Async Testing
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);
});
});TDD
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 definedRED: Write a test that describes the behavior you want. It fails because the code doesn't exist yet.
1 / 2
FAQ
Common questions about testing.