π 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.
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.
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.
| Hook | Runs | Typical use |
|---|---|---|
beforeAll | Once, before all tests in the block | Open a DB connection, start a server |
beforeEach | Before every test | Reset to a known state (fresh fixture) |
afterEach | After every test | Clear mocks, undo side effects |
afterAll | Once, after all tests in the block | Close 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
β οΈ 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:
- Create
formValidator.jsexportingvalidateEmail,validatePassword, andvalidateUsername. - 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. - Write
formValidator.test.jswith adescribeper function and both passing and failing cases. - Run
npx jest --coverageand 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,
expectassertions, mocks, and snapshots with near-zero config. - Structure tests as ArrangeβActβAssert, group with
describe, and reset state inbeforeEach. - Pick matchers deliberately β
toBefor primitives,toEqualfor objects,toThrowfor errors. - Mocks isolate a unit and let you assert how collaborators were called.
- Coverage is a diagnostic, not a target β assert meaningfully.
π Further Reading
- Jest β official documentation
- Jest β the full matcher reference
- Vitest β a Jest-compatible, ESM-native alternative
π 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.