Tutorial JavaScript Testing Jest Vitest Mocking TDD Developer Tools

Free JavaScript Testing Patterns Cheat Sheet Online — Interactive Deep-Dive Reference

· 22 min read

Testing is not a phase at the end of development. It is a continuous practice that shapes how you write code, how you structure applications, and how confidently you deploy. The developers who ship with confidence are not those who write the most tests. They are the ones who understand javascript testing patterns at a deep level — when to mock, when to integrate, how to test async code, how to organize test suites, and how to measure quality beyond line counts.

Our free interactive JavaScript Testing Patterns Cheat Sheet maps these patterns into a browsable, searchable reference. Every entry includes a concise explanation, a copyable code example, and cross-references to related concepts. The Circuit Board Test Bench aesthetic — deep industrial slate background, oscilloscope grid overlay, green pass and red fail LED indicators, signal wave traces at the page bottom, Exo 2 display headings, Nunito body text, JetBrains Mono code — turns testing reference into a quality control station experience. Everything runs client-side in your browser with no server interaction, no signup, and no data collection.

Skip ahead: If you want to explore the tool while reading, open the JavaScript Testing Patterns Cheat Sheet in another tab. Use it to test the code examples in this article in real time.

Unit Testing Fundamentals — The Foundation of Confidence

Unit tests verify the smallest testable pieces of code in isolation. A well-written unit test is fast, deterministic, and focused on a single behavior. The foundational tools are jest testing or vitest matchers that express intent clearly: toBe for primitives, toEqual for deep object comparison, toContain for arrays and strings, toThrow for error conditions, and toMatch for regular expressions.

Choosing the Right Matcher

The difference between a good test and a brittle test often comes down to matcher choice. Use toBe for strict identity (numbers, booleans, strings). Use toEqual for structural equality (objects, arrays). Use toStrictEqual when you also want to catch undefined properties and prototype mismatches. Use toBeCloseTo for floating-point arithmetic where 0.1 + 0.2 !== 0.3 in JavaScript.

import { test, expect } from 'vitest';

test('primitive equality', () => {
  expect(2 + 2).toBe(4);
  expect(true).toBe(true);
});

test('deep object equality', () => {
  expect({ a: 1, b: { c: 2 } }).toEqual({ a: 1, b: { c: 2 } });
});

test('floating point needs toBeCloseTo', () => {
  expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
});

The toHaveProperty matcher checks nested object paths with optional value matching. The toBeInstanceOf matcher verifies class relationships. The toMatchObject matcher performs partial object matching, useful when you only care about a subset of fields.

Testing Error Conditions

Functions that throw errors are as important to test as functions that return values. The toThrow matcher accepts strings, regular expressions, or error constructors. Always test both the happy path and the error path. A function with no error tests has an incomplete contract.

function divide(a, b) {
  if (b === 0) throw new Error('Cannot divide by zero');
  return a / b;
}

test('divide throws on zero', () => {
  expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
  expect(() => divide(10, 0)).toThrow(/divide by zero/i);
  expect(() => divide(10, 0)).toThrow(Error);
});

Mocking and Spies — Isolating the Unit Under Test

Unit tests must be isolated. If a test fails, you should know immediately whether the bug is in the function under test or in a dependency. Mocking replaces external dependencies with controlled substitutes. Spying observes real functions without replacing them. Together they are the most powerful tools in the javascript testing patterns arsenal.

Creating Mock Functions

vi.fn() (Vitest) and jest.fn() (Jest) create mock functions that record every call, argument, and return value. You can configure return values, implement custom logic, and assert on interaction patterns. This is essential for testing functions that accept callbacks or call external APIs.

import { vi, test, expect } from 'vitest';

const mockFetch = vi.fn();
mockFetch.mockResolvedValue({ json: () => ({ id: 1, name: 'John' }) });

const user = await mockFetch('/api/user/1').then(r => r.json());
expect(user.name).toBe('John');
expect(mockFetch).toHaveBeenCalledWith('/api/user/1');
expect(mockFetch).toHaveBeenCalledTimes(1);

Spying on Real Methods

vi.spyOn() and jest.spyOn() wrap existing methods, preserving their original behavior while recording calls. This is useful when you want to verify that a method was called but still need its real implementation to run. Always call mockRestore() after spying to clean up and prevent test pollution.

const calculator = {
  add: (a, b) => a + b,
  multiply: (a, b) => a * b,
};

const spy = vi.spyOn(calculator, 'add');
calculator.add(2, 3);
expect(spy).toHaveBeenCalledWith(2, 3);
expect(calculator.add(2, 3)).toBe(5); // Original behavior preserved
spy.mockRestore();

Mocking Entire Modules

vi.mock() and jest.mock() replace entire modules with mock implementations. Use the factory parameter to control what each export returns. For partial mocking, use vi.importActual() or jest.requireActual() to keep the real module and override only specific exports.

vi.mock('./api', async () => {
  const actual = await vi.importActual('./api');
  return {
    ...actual,
    fetchUser: vi.fn(() => Promise.resolve({ id: 1 })),
  };
});

Fake Timers for Time-Based Code

Testing code with setTimeout, setInterval, or Date is notoriously flaky. Fake timers replace the real clock with a controllable one. Advance time deterministically, run all pending timers, or advance to the next timer. This makes debounce, throttle, polling, and animation tests fast and reliable.

beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

test('debounce calls function after delay', () => {
  const fn = vi.fn();
  const debounced = debounce(fn, 100);

  debounced();
  debounced();
  debounced();
  expect(fn).not.toHaveBeenCalled();

  vi.advanceTimersByTime(100);
  expect(fn).toHaveBeenCalledTimes(1);
});

Snapshot Testing — Regression Detection at Scale

Snapshot testing captures the serialized output of a value and compares it against a stored baseline on subsequent runs. It is the most efficient way to detect unintended changes in complex objects, component renders, and API responses. The key is to use snapshot property matchers for volatile fields like timestamps, random IDs, and dates.

expect(user).toMatchSnapshot({
  id: expect.any(String),
  createdAt: expect.any(Date),
});

Inline snapshots live inside your test file instead of a separate .snap file. They are easier to review in pull requests but can make test files verbose. Named snapshots help when a single test generates multiple snapshots. Custom serializers control how specific types are written to snapshot files.

Async Testing — Promises, Callbacks, and Timing

Modern JavaScript is asynchronous. Testing async code requires understanding how your framework handles promises. The golden rule: if your test involves promises, make the test callback async and await your assertions.

Testing Promises

Use resolves and rejects matchers for promise assertions. These must be awaited, or the test will complete before the assertion runs. This is one of the most common mistakes in async testing.

// Correct — must await
await expect(fetchUser(1)).resolves.toEqual({ id: 1 });
await expect(fetchUser(-1)).rejects.toThrow('Invalid ID');

// Common mistake — missing await
expect(fetchUser(1)).resolves.toEqual({ id: 1 }); // Test may pass before promise resolves

Sequential vs Parallel Async

When async operations depend on each other, run them sequentially. When they are independent, run them in parallel with Promise.all or Promise.allSettled. Sequential tests are easier to debug. Parallel tests run faster but can create race conditions if they share mutable state.

// Sequential — each step depends on the previous
const user = await createUser({ name: 'John' });
const updated = await updateUser(user.id, { age: 30 });
expect(updated.age).toBe(30);

// Parallel — independent operations
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
expect(users).toHaveLength(3);
expect(posts).toHaveLength(5);

Waiting for Async Conditions

When testing UI code, you often need to wait for an element to appear, a state to update, or an animation to complete. The waitFor utility from Testing Library polls a condition until it passes or a timeout expires. For custom polling, implement a waitForCondition helper with configurable timeout and interval.

Test Organization and Hooks — Scaling the Test Suite

A test suite with a hundred tests is manageable. A test suite with ten thousand tests requires discipline. Organization is the difference between a test suite that accelerates development and one that slows it down.

describe, beforeEach, and afterEach

Group related tests with describe blocks. Use beforeEach and afterEach for per-test setup and cleanup. Use beforeAll and afterAll for per-suite setup that is expensive to repeat. Nest describe blocks to mirror your module structure.

describe('UserService', () => {
  let db;
  beforeAll(async () => { db = await createTestDatabase(); });
  afterAll(async () => { await db.destroy(); });

  beforeEach(async () => { await db.seed(); });
  afterEach(async () => { await db.clear(); });

  describe('createUser', () => {
    test('creates user with valid data', async () => {});
    test('rejects duplicate email', async () => {});
  });
});

Parameterized Tests

When the same test logic applies to multiple inputs, use parameterized tests. Both Jest and Vitest support test.each with array or object syntax. This reduces duplication and makes it easy to add new test cases.

test.each([
  [1, 1, 2],
  [2, 3, 5],
  [10, 20, 30],
])('add(%i, %i) = %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

Selective Test Execution

test.only runs only the specified test, useful for debugging. test.skip skips a known failure. test.todo marks a planned test. test.concurrent runs tests in parallel within a describe block. Use these judiciously — committed .only or .skip calls are common code review catches.

Coverage and Quality Metrics — Beyond Line Counts

Code coverage measures which parts of your code were executed during tests. It is a useful metric but a dangerous target. 100% line coverage with no assertion tests is worse than 70% coverage with meaningful assertions. Understand the four coverage dimensions: statement, branch, function, and line coverage.

Statement coverage counts executed lines. Branch coverage counts taken paths through conditionals. Function coverage counts called functions. Line coverage is the most common metric but can be misleading — a ternary on two lines can have 66% line coverage while having 50% branch coverage.

Set thresholds in your configuration to prevent coverage regression in CI. Use Istanbul for precise branch coverage or V8 for faster execution. Configure reporters for text output in CI and HTML output for local exploration. Exclude test files, configuration, and generated code from coverage reports.

Property-Based and Fuzz Testing — Finding Edge Cases Automatically

Traditional example-based testing verifies that code works for specific inputs. Property-based testing verifies that code works for all inputs by generating hundreds of random cases and checking that properties hold. If a property fails, the framework shrinks the input to the minimal counterexample.

import fc from 'fast-check';

test('addition is commutative', () => {
  fc.assert(
    fc.property(fc.integer(), fc.integer(), (a, b) => {
      return a + b === b + a;
    })
  );
});

Common properties to test include identity (f(x, identity) === x), associativity ((a + b) + c === a + (b + c)), and idempotence (f(f(x)) === f(x)). Fast-check provides generators for primitives, arrays, objects, UUIDs, email addresses, and URLs. You can compose custom generators for domain-specific types.

Model-based testing verifies a complex system against a simple reference model. For example, test a real Set implementation against a simple array model by generating sequences of add, delete, and has operations and comparing the state after each operation.

E2E and Integration Testing — Testing Real Behavior

Unit tests verify code in isolation. Integration tests verify that modules work together. End-to-end tests verify that the entire application works from the user's perspective. The testing pyramid recommends 70% unit, 20% integration, and 10% E2E tests for optimal coverage and speed.

API Integration Tests

Integration tests hit real HTTP endpoints with actual database interactions. Use supertest for Express applications or similar tools for other frameworks. Clean up database state between tests with transactions or truncation. Test the full request-response cycle including status codes, headers, and response bodies.

import request from 'supertest';
import { app } from './app';

describe('POST /users', () => {
  afterEach(async () => { await db.users.deleteMany(); });

  test('creates user', async () => {
    const res = await request(app)
      .post('/users')
      .send({ name: 'John', email: 'john@example.com' });

    expect(res.status).toBe(201);
    expect(res.body.id).toBeDefined();
  });
});

Browser E2E with Playwright

Playwright automates real browsers for end-to-end testing. It supports Chromium, Firefox, and WebKit with a single API. Fixtures provide reusable page objects and authentication state. Visual regression testing compares screenshots to detect unintended UI changes. Trace viewers help debug flaky tests with DOM snapshots, network logs, and console output.

Database Testing Strategies

The two common approaches for database testing are transaction rollback and test database seeding. Transaction rollback wraps each test in a database transaction that is rolled back after the test, providing perfect isolation. Testcontainers spin up real databases in Docker containers for maximum realism.

TDD and BDD Patterns — Testing as Design

Test-Driven Development is not about testing. It is about design. The Red-Green-Refactor cycle forces you to write code that is testable by construction. If a function is hard to test, that is feedback that the function has too many responsibilities or too many dependencies.

Red-Green-Refactor

Write a failing test. Write the minimal code to make it pass. Refactor the code while keeping the test green. The constraint of having a test before the implementation leads to smaller functions, clearer interfaces, and decoupled modules.

Given-When-Then

Behavior-Driven Development uses Given-When-Then structure to express tests in business language. Given establishes context. When describes the action. Then asserts the outcome. This pattern produces test names that read like specifications.

test('given a logged-in user, when they add item to cart, then cart count increases', () => {
  // Given
  const user = createLoggedInUser();
  const product = createProduct({ price: 10 });

  // When
  user.addToCart(product);

  // Then
  expect(user.cart.count).toBe(1);
  expect(user.cart.total).toBe(10);
});

Arrange-Act-Assert

The classic test structure separates setup, execution, and verification into three distinct sections. Each section should be exactly one conceptual step. If your Arrange section is ten lines long, the function under test probably has too many dependencies. If your Assert section has ten unrelated checks, the test is verifying too many behaviors.

The Testing Pyramid

The testing pyramid illustrates the ideal balance of test types. Unit tests form the wide base — fast, isolated, and numerous. Integration tests form the middle — slower but verify module interactions. E2E tests form the narrow top — slowest but most realistic. Anti-patterns include the ice cream cone (too many E2E, few unit) and the hourglass (many unit, many E2E, missing integration).

Advanced Patterns — Dependency Injection, Factories, and Contracts

As applications grow, testing requires architectural patterns. Dependency injection makes code testable by passing dependencies as parameters rather than importing them directly. Test factories create data with sensible defaults and override capabilities. Builder patterns provide fluent APIs for constructing complex test objects.

function createUser(overrides = {}) {
  return {
    id: Math.random().toString(36),
    name: 'John Doe',
    email: 'john@example.com',
    role: 'user',
    ...overrides,
  };
}

const admin = createUser({ role: 'admin', name: 'Admin User' });

Contract testing with Pact verifies that API consumers and providers agree on data contracts without running both sides simultaneously. Mutation testing with Stryker evaluates test quality by introducing artificial bugs and checking if tests catch them. A survived mutant indicates a gap in the test suite.

Load testing with k6 verifies system behavior under expected and peak traffic. Security testing validates input sanitization, parameterized queries, and authentication boundaries. Visual regression testing captures and compares screenshots to detect unintended UI changes.

Summary / TL;DR

PatternCategoryUse CaseKey API
toBe / toEqualUnit TestingAssert values and structuresexpect(x).toBe(y)
Mock functionsMockingReplace dependenciesvi.fn(), mockReturnValue()
SpiesMockingObserve real methodsvi.spyOn(), mockRestore()
Module mockingMockingMock entire modulesvi.mock(), importActual()
SnapshotsRegressionLock output contractstoMatchSnapshot()
Async testsAsyncTest promisesasync test() + await
Fake timersAsyncControl timevi.useFakeTimers()
HooksOrganizationSetup and cleanupbeforeEach(), afterAll()
ParameterizedOrganizationData-driven teststest.each()
CoverageQualityMeasure test reach--coverage
Property-basedGenerativeFind edge casesfast-check
API integrationIntegrationTest real endpointssupertest
Browser E2EE2EFull user flowsPlaywright, Cypress
TDD cycleProcessDesign through testsRed-Green-Refactor
DI patternArchitectureMake code testableInject dependencies
Contract testingAdvancedVerify API contractsPact
Mutation testingAdvancedEvaluate test qualityStryker

Try the Free Interactive Cheat Sheet

This article covers the concepts, but the JavaScript Testing Patterns Cheat Sheet puts every pattern at your fingertips. Open it in your browser, search for any API, copy code examples with one click, and browse by category. It is free, requires no registration, and runs entirely client-side.

JavaScript testing patterns are not optional extras for large teams. They are the practices that enable confident refactoring, safe deployments, and maintainable codebases. Master matchers and you can express any assertion. Master mocking and you can isolate any dependency. Master async testing and you can verify any promise. Master organization and you can scale to thousands of tests. Master coverage and you know where the gaps are. Master property-based testing and you find bugs you never thought to look for. These patterns are worth knowing deeply.

Related Resources

Every tool in the DevToolkit collection is free, requires no signup, and runs entirely in your browser. Build faster by keeping the right reference at your fingertips.

Found this useful? Check out our free developer tools or browse more articles.