๐งช Introduction to Jest
Every professional codebase you'll ever touch is held together by an invisible safety net: its tests. Jest is the framework that spins that net for you in the JavaScript world โ batteries included, almost zero setup. In this lesson you'll install it, write your very first passing test, and learn the small vocabulary of functions that powers thousands of real-world test suites.
Week 3 · Day 5 (Friday: Testing Fundamentals) · Lecture 1
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a test framework does and why Jest is the default choice for JavaScript projects
- Install and configure Jest in a Node project with npm scripts
- Write a test using
describe,test, andexpect - Choose the right matcher (
toBe,toEqual,toThrow, and friends) for each assertion - Structure a test with the Arrange-Act-Assert pattern
- Run tests once, in watch mode, and with a coverage report
Estimated Time: 60 minutes
Practice: Test a small math module and a password strength checker from scratch.
In This Lesson
The Safety Net
Picture a trapeze artist working high above the crowd. The daring flips only look effortless because there is a net below โ one that turns a potential disaster into a harmless bounce. In software, automated tests are that net. They let you refactor boldly, ship on Friday, and sleep on the weekend, because the moment a change breaks something, a test goes red and tells you exactly where.
Testing is not one single activity. It comes in layers, each answering a different question about your code. Today's lesson lives in the bottom, widest layer โ unit tests โ but it helps to see the whole pyramid first.
in isolation] B --> B2[Fast โ run in milliseconds] B --> B3[Many of them] C --> C1[Test modules working together] C --> C2[e.g. service + database] D --> D1[Test a full user journey] D --> D2[Slow โ run in a real browser]
A healthy project has many fast unit tests at the base, fewer integration tests in the middle, and a handful of slow end-to-end tests at the top. Jest is the tool we'll use for the base, and the same skills carry upward into the other layers.
Why Jest?
Jest is an open-source testing framework maintained by Meta and now stewarded by the OpenJS Foundation. Think of it as a Swiss Army knife: instead of gluing together a test runner, an assertion library, and a mocking tool, you get all three in one install that "just works" for most JavaScript projects.
๐ What a test framework actually gives you
Three jobs, one tool. A runner finds your test files and executes them. An assertion library (Jest's expect) lets you state "this should equal that" and fails loudly when it doesn't. A mocking library lets you swap out slow or unpredictable dependencies for controlled fakes.
Key features
- Zero configuration: point it at a project and it runs โ no config file required to start
- Fast & parallel: test files run in parallel worker processes
- Rich matchers: a large, readable vocabulary of assertions
- Built-in mocking:
jest.fn(), module mocks, and fake timers, no add-ons - Code coverage: a coverage report with a single flag
- Watch mode: re-runs only the tests affected by your last edit
- Helpful failures: colored diffs that point straight at the mismatch
๐ก Jest vs. Vitest
You'll also hear about Vitest, a newer runner built for Vite projects. Its API is intentionally almost identical to Jest's โ describe, test, expect all behave the same. Learn Jest well and Vitest is a five-minute switch.
Installing & Configuring Jest
Jest is a development-only dependency โ you need it while building, not when your app runs in production. Install it with the --save-dev flag inside any project that already has a package.json.
# Install Jest as a dev dependency
npm install --save-dev jest
# Verify the install
npx jest --version
Add npm scripts
Typing npx jest works, but a script is friendlier for your team. Add these to package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Now npm test runs the whole suite once, npm run test:watch keeps it running as you code, and npm run test:coverage tells you which lines your tests actually touched.
A config file (only when you need one)
Jest runs with sensible defaults, so you often need no config at all. When you do โ for example, to test browser-style code that expects a document โ add a jest.config.js file:
// jest.config.js
/** @type {import('jest').Config} */
module.exports = {
// 'node' for backend code; 'jsdom' simulates a browser DOM.
// (For jsdom you must also: npm install --save-dev jest-environment-jsdom)
testEnvironment: 'node',
// Which files count as tests. This is the default โ shown for clarity.
testMatch: ['**/__tests__/**/*.[jt]s', '**/?(*.)+(spec|test).[jt]s'],
// Collect coverage from your source, but ignore entry points.
collectCoverageFrom: ['src/**/*.js', '!src/index.js'],
};
โ ๏ธ ESM vs. CommonJS
Older Jest examples use module.exports/require (CommonJS). Modern projects often use import/export (ES modules). If you write import in your source, add Babel (babel-jest plus @babel/preset-env) or set "type": "module" and run Jest with the experimental VM flag. In this lesson we use ESM-style import/export in examples and assume a Babel transform is present, which is the norm in real projects.
Your First Test
Let's test something tiny and real. Create a module with a few pure math functions, then a test file that proves they behave. Jest automatically finds any file ending in .test.js.
The code under test
// math.js
export function add(a, b) {
return a + b;
}
export function divide(a, b) {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}
The test file
// math.test.js
import { add, divide } from './math';
// describe() groups related tests and names the "unit" under test.
describe('math', () => {
// Each test() (alias: it()) is one specific expectation.
test('adds two positive numbers', () => {
expect(add(1, 2)).toBe(3);
});
test('adds negative numbers correctly', () => {
expect(add(-1, -1)).toBe(-2);
});
test('divides evenly', () => {
expect(divide(6, 2)).toBe(3);
});
test('throws when dividing by zero', () => {
// Note: pass a FUNCTION to expect, so Jest can call it and catch the throw.
expect(() => divide(5, 0)).toThrow('Cannot divide by zero');
});
});
Run it with npm test and you'll see something like:
Output
PASS ./math.test.js
math
โ adds two positive numbers (2 ms)
โ adds negative numbers correctly
โ divides evenly
โ throws when dividing by zero (1 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Read the anatomy of that first test out loud: "expect the result of add(1, 2) toBe 3." That readability is the whole point โ a good test doubles as documentation.
Test Structure & the AAA Pattern
Well-written tests share a shape, like chapters in a book. Inside every test body, most people follow Arrange, Act, Assert โ set up the world, do the thing, then check the result. Separating those three phases (even with blank lines) makes a test instantly scannable.
test('updating a name leaves the email untouched', () => {
// Arrange โ set up the test data and conditions
const user = { id: 1, name: 'John Doe', email: 'john@example.com' };
// Act โ execute the code being tested
const updated = updateProfile(user, { name: 'John Smith' });
// Assert โ check the results
expect(updated.name).toBe('John Smith');
expect(updated.email).toBe('john@example.com');
});
๐ก test vs it
Jest gives you both test('...') and it('...') โ they are literally the same function. it reads nicely as a sentence: it('returns 0 for an empty string'). Pick one style per project and stay consistent.
Matchers
A matcher is the part after expect(value) that describes what you expect. Each one is like a different lens for inspecting a result. Here are the ones you'll use constantly.
Equality: toBe vs toEqual
// toBe uses Object.is โ great for numbers, strings, booleans (primitives).
expect(2 + 2).toBe(4);
// toEqual recursively compares the CONTENTS of objects and arrays.
expect({ name: 'John' }).toEqual({ name: 'John' });
expect([1, 2, 3]).toEqual([1, 2, 3]);
// This is the classic beginner bug:
// expect({ name: 'John' }).toBe({ name: 'John' }); // โ FAILS โ different objects in memory
โ ๏ธ The #1 matcher mistake
Two objects with identical contents are not the same object. toBe checks identity, so it fails on separate objects even when they look equal. For anything that isn't a primitive, reach for toEqual.
Truthiness & numbers
expect(true).toBeTruthy();
expect(0).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hi').toBeDefined();
expect(10).toBeGreaterThan(5);
expect(5).toBeLessThanOrEqual(5);
// Floating point? Use toBeCloseTo, never toBe:
expect(0.1 + 0.2).toBeCloseTo(0.3); // โ
(0.1 + 0.2 is 0.30000000000000004)
Strings, arrays & errors
expect('Hello World').toMatch(/World/); // regex or substring
expect(['apple', 'banana']).toContain('banana');
expect([1, 2, 3]).toHaveLength(3);
// Testing that code throws โ pass a function wrapper:
expect(() => {
throw new Error('Invalid input');
}).toThrow('Invalid input');
Negation with .not
expect('team').not.toMatch(/I/);
expect([1, 2]).not.toContain(3);
โ Custom matchers
When you write the same assertion over and over, teach Jest a new matcher with expect.extend:
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
`expected ${received} ${pass ? 'not ' : ''}to be within ${floor}โ${ceiling}`,
};
},
});
test('score is in range', () => {
expect(100).toBeWithinRange(90, 110);
expect(101).not.toBeWithinRange(0, 100);
});
Testing Asynchronous Code
Most real code waits on something โ a network request, a database, a timer. Testing async code is like ordering a pizza: you have to actually wait for delivery before you check the order. The golden rule is to return or await the promise so Jest knows to wait too.
// A function that resolves or rejects a promise:
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
id === 1
? resolve({ id: 1, name: 'John Doe' })
: reject(new Error('User not found'));
}, 50);
});
}
// Cleanest style: async/await
test('fetches a user by id', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('John Doe');
});
// The .resolves / .rejects helpers read nicely too:
test('resolves to the right object', async () => {
await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'John Doe' });
});
test('rejects for a missing user', async () => {
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
โ ๏ธ The silent-pass trap
Forget the await (or return) and Jest finishes the test before the promise settles. The assertion never runs, and the test passes green even though it checked nothing. Always await your async expectations.
Running Tests & Watch Mode
Once tests exist, you'll run them constantly. Three commands cover almost everything.
| Command | What it does | When to use it |
|---|---|---|
npm test | Runs every test once | Before committing; in CI |
npm run test:watch | Re-runs affected tests on save | While actively coding |
npm run test:coverage | Runs all tests + a coverage table | Checking how much is tested |
Watch mode is your day-to-day companion. It watches for file changes and, by default, only re-runs tests related to what you edited. Inside watch mode you can press keys to filter:
Watch Usage
โบ Press a to run all tests.
โบ Press f to run only failed tests.
โบ Press p to filter by a filename regex pattern.
โบ Press t to filter by a test-name regex pattern.
โบ Press q to quit watch mode.
โบ Press Enter to trigger a test run.
A coverage report shows the percentage of statements, branches, functions, and lines your tests exercised โ and, crucially, the line numbers they missed:
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 85.71 | 100 | 83.33 | 85.71 |
math.js | 100 | 100 | 100 | 100 |
userService.js | 71.43 | 100 | 50 | 71.43 | 7-9
--------------------|---------|----------|---------|---------|-------------------
๐ก Coverage is a map, not a grade
100% coverage means every line ran during tests โ not that every line is correct. Chase meaningful assertions, not a vanity number. That said, uncovered lines (like 7-9 above) are an honest to-do list.
Practice & Quiz
๐๏ธ Exercise 1: Test a math module
Goal: Given the module below, write a test suite that covers the happy path and the error case.
// calc.js
export function multiply(a, b) {
return a * b;
}
export function percentOf(part, whole) {
if (whole === 0) throw new Error('whole cannot be zero');
return (part / whole) * 100;
}
๐ก Hint
Group the tests with describe('calc', ...). Use toBe for the numeric results, toBeCloseTo if a result isn't a clean integer, and expect(() => percentOf(1, 0)).toThrow(...) for the error.
โ Solution
import { multiply, percentOf } from './calc';
describe('calc', () => {
test('multiplies two numbers', () => {
expect(multiply(3, 4)).toBe(12);
});
test('computes a percentage', () => {
expect(percentOf(25, 200)).toBe(12.5);
});
test('throws when whole is zero', () => {
expect(() => percentOf(1, 0)).toThrow('whole cannot be zero');
});
});
๐๏ธ Exercise 2: Test an async fetch
Goal: Write two tests for getTitle โ one for success, one for a rejected promise. Use a mock function so no real network call happens.
// title.js
export async function getTitle(api, id) {
const post = await api.fetchPost(id);
return post.title.toUpperCase();
}
โ Solution
import { getTitle } from './title';
test('uppercases the fetched title', async () => {
const api = { fetchPost: jest.fn().mockResolvedValue({ title: 'hello' }) };
await expect(getTitle(api, 1)).resolves.toBe('HELLO');
expect(api.fetchPost).toHaveBeenCalledWith(1);
});
test('propagates a fetch error', async () => {
const api = { fetchPost: jest.fn().mockRejectedValue(new Error('boom')) };
await expect(getTitle(api, 1)).rejects.toThrow('boom');
});
๐ฏ Quick Quiz
Question 1: Which matcher should you use to compare two objects by their contents?
Question 2: How do you assert that a function throws an error?
Question 3: In watch mode, what does an async test with a forgotten await most often do?
Best Practices & Pitfalls
โ Do
- Name tests as behavior sentences:
'throws when dividing by zero' - Keep each test focused on one assertion of behavior
- Use
toEqualfor objects/arrays,toBefor primitives - Always
await(orreturn) async expectations - Run watch mode while coding so feedback is instant
โ Don't
- Write
test('test 1', ...)โ a failing name should explain itself - Compare floats with
toBeโ usetoBeCloseTo - Call a throwing function directly inside
expect() - Chase 100% coverage at the cost of meaningful assertions
โ ๏ธ Tests that pass no matter what
The most dangerous test is one that can never fail. A forgotten await, an expect with no matcher, or an assertion inside an if that never runs โ all pass green while checking nothing. When you write a test, briefly break the code on purpose and confirm the test goes red. In the next lesson, and with TDD, we make that "see it fail first" step a habit.
Summary
๐ Key Takeaways
- Automated tests are a safety net โ unit tests form the fast, plentiful base of the pyramid
- Jest bundles a runner, assertions, and mocking with near-zero config
- The core trio is
describe/test/expect, structured as Arrange-Act-Assert - Pick matchers deliberately:
toBefor primitives,toEqualfor objects,toThrowfor errors,toBeCloseTofor floats - Async tests must
awaitthe promise, or they pass without checking - Live in watch mode while coding; use coverage as a map of what's untested
๐ Additional Resources
- Jest โ Getting Started
- Jest โ Expect & the full matcher list
- Jest โ Testing Asynchronous Code
- MDN โ Tools and testing
๐ What's Next?
You can now write and run a test. Next we go deeper into the craft: what makes a unit test good โ fast, isolated, repeatable โ and how to test pure functions, classes, and error paths with confidence. That's Writing Unit Tests.
๐ Your safety net is up!
You wrote your first passing tests. From here, every feature you build can be checked automatically โ that's the confidence great developers rely on.