Testing with Jest
Tests check that your code works now, and they keep checking it after you change the code later. Without tests, every refactor risks introducing bugs. The JavaScript ecosystem has several mature testing tools that are easy to use, and Jest is still one of the most widely used.
Types of Tests and the Testing Pyramid
There are three main categories:
| Type | Scope | Speed | Purpose |
|---|---|---|---|
| Unit | Single function or module | Fast | Test isolated logic |
| Integration | Multiple modules together | Medium | Test how components interact |
| End-to-End (E2E) | Entire application | Slow | Test real user workflows |
The testing pyramid is a common guideline: write many unit tests, fewer integration tests, and even fewer E2E tests. Unit tests are cheap and fast. E2E tests are expensive and slow. My advice is to start with unit tests and add the others as your project grows.
Setting Up Jest
Install Jest as a development dependency:
npm install --save-dev jest
Then add test scripts to your package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Jest looks for files ending in .test.js (or .spec.js) by default. No extra configuration is needed for a basic CommonJS setup.
Writing Tests with describe, test, and expect
Say we have a small module:
// math.js
function add(a, b) {
return a + b;
}
function divide(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
module.exports = { add, divide };
Here is how you would test it:
// math.test.js
const { add, divide } = require("./math");
describe("Math functions", () => {
describe("add", () => {
test("adds two positive numbers", () => {
expect(add(1, 2)).toBe(3);
});
test("adds negative numbers", () => {
expect(add(-1, -2)).toBe(-3);
});
});
describe("divide", () => {
test("divides two numbers", () => {
expect(divide(10, 2)).toBe(5);
});
test("throws on division by zero", () => {
expect(() => divide(10, 0)).toThrow("Division by zero");
});
});
});
If your project uses native ES modules, Jest can handle that too, but you will need the ESM setup described in the Jest docs ("type": "module" and/or transform config).
The describe blocks group related tests. The test function defines a single test case. And expect creates an assertion, a claim about what a value should be.
Common Matchers for Assertions
Jest gives you many matchers that you chain onto expect():
// Equality
expect(value).toBe(3); // strict equality (===)
expect(value).toEqual({ a: 1 }); // deep equality for objects/arrays
expect(value).not.toBe(4); // negation
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThan(10);
expect(value).toBeCloseTo(0.3, 5); // for floating point comparisons
// Strings
expect(str).toMatch(/pattern/);
expect(str).toContain("substring");
// Arrays
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
// Objects
expect(obj).toHaveProperty("key");
expect(obj).toMatchObject({ a: 1 });
// Exceptions
expect(() => fn()).toThrow();
expect(() => fn()).toThrow("error message");
expect(() => fn()).toThrow(TypeError);
Use toBe for primitives (numbers, strings, booleans) and toEqual for objects and arrays.