Skip to main content

πŸƒ Jest Testing Framework

Jest is the batteries-included testing framework that most JavaScript teams reach for first. In this lesson you'll install it, learn the describe/test structure, meet the matcher family, mock a dependency, measure coverage, and write a complete suite for a real shopping-cart module.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Install and configure Jest in a Node project and run it from an npm script
  • Structure tests with describe, test, and the setup/teardown hooks
  • Choose the right matcher for equality, numbers, strings, arrays, and thrown errors
  • Write a realistic test suite and use mocks to isolate dependencies
  • Generate a coverage report and read it critically

Estimated Time: 35–45 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Test-drive a form-validation utility, then verify its coverage.

In This Lesson

What Is Jest?

Jest is a JavaScript testing framework, originally built at Facebook (now Meta) and today maintained under the OpenJS Foundation. Its defining feature is that it is a complete solution in one package: a test runner, an assertion (expect) library, a mocking system, and snapshot testing all ship together and work with almost no configuration.

πŸ’‘ A useful analogy: If shipping software were like running a kitchen, Jest is the tasting station β€” a fixed spot where every dish is checked against the recipe before it leaves for the customer. It doesn't cook for you, but it will tell you, instantly and precisely, when something is off.

You'll find Jest under many other names in the ecosystem β€” Vitest is a modern, Vite-native runner with a nearly identical API β€” so learning Jest's model transfers almost directly. Everything you learn here about describe, test, and matchers applies to Vitest too.

What Jest bundles in one package Four capabilities β€” test runner, assertion library, mocking, and snapshots β€” all provided by a single Jest package with zero configuration. Jest one package Β· zero config Test runner finds & runs tests expect() assertions jest.fn() mocks & spies Snapshots output diffing
Figure 1 β€” Jest bundles four separate concerns into one dependency, which is why it needs almost no setup.

Getting Started

Setting up Jest takes three small steps: initialise the project, install Jest as a dev dependency, and wire up a test script.

# 1. Initialise npm (skip if package.json already exists)
npm init -y

# 2. Install Jest as a development dependency
npm install --save-dev jest

Then add a test script to package.json:

{
  "scripts": {
    "test": "jest"
  }
}

πŸ“– CommonJS vs. ESM

These examples use CommonJS (require/module.exports) because it works with Jest out of the box. To use import/export syntax, either set "type": "module" and run Jest with node --experimental-vm-modules, or add Babel. If you'd rather skip that friction entirely, Vitest supports ESM natively.

Your first test

Create a tiny function in sum.js:

// sum.js
function sum(a, b) {
  return a + b;
}

module.exports = sum;

Now a test file next to it, sum.test.js. Jest automatically finds files that end in .test.js or .spec.js, or that live in a __tests__ folder:

// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

Run it with npm test. You should see a green checkmark and a summary of one passing test.

flowchart LR A[Write source function] --> B[Write .test.js file] B --> C["expect(...).matcher()"] C --> D[Run npm test] D --> E{Green?} E -->|Yes| F[Ship with confidence] E -->|No| G[Fix code or test] G --> D

Test Structure & Hooks

A good test reads like a small story with a predictable shape. The industry name for that shape is Arrange–Act–Assert (sometimes "Given–When–Then"):

test('describes the behaviour being verified', () => {
  // Arrange β€” set up data and preconditions
  const cart = new ShoppingCart();

  // Act β€” call the thing under test
  cart.addItem({ name: 'Keyboard', price: 50 });

  // Assert β€” check the outcome
  expect(cart.getTotal()).toBe(50);
});

Related tests are grouped inside a describe block, which nests in the output and lets you share setup:

describe('Math utilities', () => {
  test('sum adds numbers', () => {
    expect(sum(2, 3)).toBe(5);
  });

  test('multiply works', () => {
    expect(multiply(3, 4)).toBe(12);
  });
});

Setup and teardown hooks

Jest gives you four hooks for preparing and cleaning up. The *Each variants run around every test; the *All variants run once for the whole block.

HookRunsTypical use
beforeAllOnce, before all tests in the blockOpen a DB connection, start a server
beforeEachBefore every testReset to a known state (fresh fixture)
afterEachAfter every testClear mocks, undo side effects
afterAllOnce, after all tests in the blockClose connections, free resources
describe('Database operations', () => {
  beforeAll(() => { /* connect once */ });
  afterAll(() => { /* disconnect once */ });

  beforeEach(() => { /* reset to a clean state before each test */ });
  afterEach(() => { /* tidy up after each test */ });

  test('can save a record', () => {
    // ...
  });
});

⚠️ Reset in beforeEach, not beforeAll

If you build your fixture once in beforeAll and mutate it in a test, later tests inherit that mutation and become order-dependent. Rebuild mutable state in beforeEach so every test starts from a clean slate.

Matchers: Verifying Results

A matcher is the method you chain after expect(value) to state what you expect. Picking the right one makes failures readable β€” Jest prints a focused diff instead of a generic "false".

Equality & truthiness

expect(2 + 2).toBe(4);                       // === for primitives
expect({ name: 'John' }).toEqual({ name: 'John' }); // deep equality
expect({ a: undefined, b: 2 }).toStrictEqual({ b: 2 }); // stricter

expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

⚠️ toBe vs. toEqual

toBe uses Object.is (reference equality) β€” perfect for numbers, strings, and booleans, but it fails on two different objects with the same contents. For objects and arrays, reach for toEqual (deep, recursive comparison) or toStrictEqual when you also care about undefined keys and class types.

Numbers

expect(10).toBeGreaterThan(9);
expect(10).toBeGreaterThanOrEqual(10);
expect(10).toBeLessThan(11);

// Floating-point maths is imprecise β€” never use toBe here
expect(0.1 + 0.2).toBeCloseTo(0.3);

Strings & arrays

expect('hello world').toMatch(/world/);   // regex
expect('hello world').toContain('world');  // substring

const list = ['milk', 'bread', 'eggs'];
expect(list).toContain('milk');
expect(list).toHaveLength(3);
expect(list).toEqual(expect.arrayContaining(['eggs', 'milk']));

Thrown errors

To assert that a function throws, wrap the call in an arrow function so Jest can invoke it inside a try/catch:

function boom() {
  throw new Error('This is an error');
}

expect(() => boom()).toThrow();
expect(() => boom()).toThrow('This is an error');
expect(() => boom()).toThrow(/error/);
πŸ’‘ Think of matchers as specialist inspectors. One checks dimensions, another checks weight, another checks that the packaging is intact. Using the specialist for the job means that when something fails, the report tells you exactly what was wrong.

Mock Functions

A mock is a stand-in you control. It lets you test a unit in isolation β€” without hitting a real database, network, or clock β€” and lets you assert how your code called its collaborators.

test('calls the callback with the total', () => {
  const onComplete = jest.fn();          // a spy that records calls

  processOrder({ total: 42 }, onComplete);

  expect(onComplete).toHaveBeenCalled();
  expect(onComplete).toHaveBeenCalledTimes(1);
  expect(onComplete).toHaveBeenCalledWith(42);
});

You can also program a mock's return value or resolve a promise from it:

const fetchUser = jest.fn();

fetchUser.mockReturnValue({ id: 1 });        // sync value
fetchUser.mockResolvedValue({ id: 1 });      // resolved promise
fetchUser.mockRejectedValueOnce(new Error('boom')); // one rejection

To replace an entire module β€” say, a database client β€” use jest.mock:

jest.mock('./database');           // auto-mocks every export
const db = require('./database');

beforeEach(() => jest.clearAllMocks()); // reset call history between tests

πŸ’‘ Clear, reset, or restore?

jest.clearAllMocks() wipes call history but keeps implementations. jest.resetAllMocks() also removes any mocked return values. jest.restoreAllMocks() puts the original implementation back (for spies created with jest.spyOn). Clearing in beforeEach is the most common default.

Worked Example: Shopping Cart

Let's test a small but realistic module β€” a shopping cart that adds items, merges duplicates, removes items, and totals the bill.

The implementation (shoppingCart.js):

// shoppingCart.js
class ShoppingCart {
  constructor() {
    this.items = [];
  }

  addItem(item) {
    if (!item.name || !item.price || item.price <= 0) {
      throw new Error('Invalid item');
    }

    const existing = this.items.find((i) => i.name === item.name);
    if (existing) {
      existing.quantity += item.quantity ?? 1;
    } else {
      this.items.push({ ...item, quantity: item.quantity ?? 1 });
    }
    return this.items;
  }

  removeItem(name) {
    const before = this.items.length;
    this.items = this.items.filter((i) => i.name !== name);
    return this.items.length < before;
  }

  getTotal() {
    return this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  }

  clear() {
    this.items = [];
  }
}

module.exports = ShoppingCart;

The test suite (shoppingCart.test.js). Notice how beforeEach gives each test a fresh cart, and how each test verifies exactly one behaviour:

// shoppingCart.test.js
const ShoppingCart = require('./shoppingCart');

describe('ShoppingCart', () => {
  let cart;

  beforeEach(() => {
    cart = new ShoppingCart();
  });

  test('starts empty', () => {
    expect(cart.items).toEqual([]);
    expect(cart.getTotal()).toBe(0);
  });

  test('adds an item with a default quantity of 1', () => {
    cart.addItem({ name: 'Keyboard', price: 50 });

    expect(cart.items).toHaveLength(1);
    expect(cart.items[0]).toMatchObject({ name: 'Keyboard', price: 50, quantity: 1 });
  });

  test('merges quantity when the same item is added twice', () => {
    cart.addItem({ name: 'Mouse', price: 25 });
    cart.addItem({ name: 'Mouse', price: 25 });

    expect(cart.items).toHaveLength(1);
    expect(cart.items[0].quantity).toBe(2);
  });

  test('respects an explicit quantity', () => {
    cart.addItem({ name: 'Headphones', price: 100, quantity: 2 });
    expect(cart.items[0].quantity).toBe(2);
  });

  test('rejects invalid items', () => {
    expect(() => cart.addItem({ price: 50 })).toThrow('Invalid item');
    expect(() => cart.addItem({ name: 'X' })).toThrow('Invalid item');
    expect(() => cart.addItem({ name: 'X', price: -10 })).toThrow('Invalid item');
  });

  test('removes an existing item and reports success', () => {
    cart.addItem({ name: 'Keyboard', price: 50 });
    expect(cart.removeItem('Keyboard')).toBe(true);
    expect(cart.items).toHaveLength(0);
  });

  test('returns false when removing something not in the cart', () => {
    expect(cart.removeItem('Ghost')).toBe(false);
  });

  test('calculates the correct total', () => {
    cart.addItem({ name: 'Keyboard', price: 50 });
    cart.addItem({ name: 'Mouse', price: 25 });
    cart.addItem({ name: 'Headphones', price: 100, quantity: 2 });

    expect(cart.getTotal()).toBe(275); // 50 + 25 + (100 Γ— 2)
  });
});

Terminal output

PASS  ./shoppingCart.test.js
  ShoppingCart
    βœ“ starts empty (2 ms)
    βœ“ adds an item with a default quantity of 1
    βœ“ merges quantity when the same item is added twice
    βœ“ respects an explicit quantity
    βœ“ rejects invalid items (1 ms)
    βœ“ removes an existing item and reports success
    βœ“ returns false when removing something not in the cart
    βœ“ calculates the correct total

Test Suites: 1 passed, 1 total
Tests:       8 passed, 8 total

With this suite in place you can refactor the cart internals fearlessly: as long as the tests stay green, the behaviour your users depend on is intact.

Coverage, Watch Mode & Snapshots

Watch mode

During development, run Jest in watch mode so it re-runs only the tests affected by files you just changed:

npx jest --watch      # re-run on change (git-aware)
npx jest --watchAll   # re-run everything on any change

Coverage

Coverage measures which lines, branches, and functions your tests exercise:

npx jest --coverage
pie showData title Example coverage report (%) "Statements" : 87 "Branches" : 75 "Functions" : 92 "Lines" : 88

⚠️ Coverage is a floor, not a goal

100% coverage only means every line ran β€” not that you asserted the right things. A test that calls a function but checks nothing still counts as coverage. Aim high on critical logic (branches matter most), but treat the number as a smoke detector, not a trophy.

Filtering tests

# Run only tests whose name matches "total"
npx jest -t "total"

# Run a single file
npx jest shoppingCart.test.js

Snapshot testing

Snapshots capture a serialised output on the first run and compare against it afterwards. They shine for stable data structures and rendered UI:

test('formats a product card', () => {
  const card = renderProductCard({ id: 1, name: 'Keyboard', price: 79.99 });
  expect(card).toMatchSnapshot();
});

On the first run Jest writes a __snapshots__ file; later runs fail if the output drifts. Review snapshot diffs like any other change, and update deliberately with jest -u β€” never blindly.

Best Practices

βœ… Do

  • Test behaviour, not implementation. Assert on outputs and observable effects, not private internals.
  • One concept per test. A focused test names the exact thing that broke.
  • Write descriptive names. "returns false when removing a missing item" beats "removeItem works".
  • Keep tests independent. Any test should pass when run alone or in any order.
  • Co-locate tests with the code they cover, or mirror the structure in __tests__.

⚠️ Avoid

  • Interdependent tests that share mutable state and break when reordered.
  • Over-mocking. If you mock everything, you test your mocks, not your code.
  • Assertion-free tests that inflate coverage without verifying anything.
  • Snapshotting huge, volatile output that nobody actually reviews.
πŸ’‘ From the field: Teams that keep test files right next to the components they cover tend to update those tests when the code changes β€” proximity nudges the right habit. Distant test folders quietly rot.

Hands-on Exercise

πŸ‹οΈ Test-drive a form validator

Objective: Write a Jest suite for a validation utility, then implement it until the suite goes green (a taste of test-first development).

Instructions:

  1. Create formValidator.js exporting validateEmail, validatePassword, and validateUsername.
  2. Rules: email must look like a@b.c; password β‰₯ 8 chars with at least one uppercase, one lowercase, and one digit; username is 3–20 alphanumeric characters.
  3. Write formValidator.test.js with a describe per function and both passing and failing cases.
  4. Run npx jest --coverage and push branch coverage above 90%.
πŸ’‘ Hint

Each rule maps cleanly to a regular expression. Test the boundaries explicitly: a 7-character password should fail, an 8-character one should pass. Boundary cases are where branch coverage is won.

βœ… Sample solution
// formValidator.js
const formValidator = {
  validateEmail: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
  validatePassword: (pw) =>
    typeof pw === 'string' &&
    pw.length >= 8 &&
    /[a-z]/.test(pw) && /[A-Z]/.test(pw) && /[0-9]/.test(pw),
  validateUsername: (u) => /^[a-zA-Z0-9]{3,20}$/.test(u ?? ''),
};

module.exports = formValidator;
// formValidator.test.js
const v = require('./formValidator');

describe('validateEmail', () => {
  test('accepts a valid address', () => {
    expect(v.validateEmail('ray@example.com')).toBe(true);
  });
  test('rejects a malformed address', () => {
    expect(v.validateEmail('ray@example')).toBe(false);
    expect(v.validateEmail('nope')).toBe(false);
  });
});

describe('validatePassword', () => {
  test('accepts a strong password', () => {
    expect(v.validatePassword('Sup3rSafe')).toBe(true);
  });
  test('rejects one that is too short', () => {
    expect(v.validatePassword('Ab3')).toBe(false);
  });
  test('rejects one missing a digit', () => {
    expect(v.validatePassword('NoDigitsHere')).toBe(false);
  });
});

describe('validateUsername', () => {
  test('accepts 3–20 alphanumerics', () => {
    expect(v.validateUsername('ray99')).toBe(true);
  });
  test('rejects symbols and bad lengths', () => {
    expect(v.validateUsername('ab')).toBe(false);
    expect(v.validateUsername('has space')).toBe(false);
  });
});

🎯 Quick Quiz

Question 1: You compare two different objects that have identical contents. Which matcher should you use?

Question 2: Where should you rebuild a mutable fixture so tests stay independent?

Question 3: What does 100% code coverage actually guarantee?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Jest is all-in-one: runner, expect assertions, mocks, and snapshots with near-zero config.
  • Structure tests as Arrange–Act–Assert, group with describe, and reset state in beforeEach.
  • Pick matchers deliberately β€” toBe for primitives, toEqual for objects, toThrow for errors.
  • Mocks isolate a unit and let you assert how collaborators were called.
  • Coverage is a diagnostic, not a target β€” assert meaningfully.

πŸ“š Further Reading

πŸš€ What's Next?

Next we'll bring these same ideas to the browser: testing React components from the user's point of view with React Testing Library, where matchers meet the DOM.

πŸŽ‰ Well done!

You can now install Jest, write structured tests, and read a coverage report. That's the foundation every other testing lesson builds on.