What is Jest?
Jest is a JavaScript test runner created at Facebook and now one of the most widely used testing tools in the ecosystem. It is batteries-included: assertions, mocking, snapshot testing and coverage all ship in one package, and a new project needs very little configuration to get started.
For years Jest was the default choice for React and Node projects, and it remains dominant in existing codebases. Even with Vitest rising, Jest’s API is the shared vocabulary of JavaScript testing, which makes it worth knowing regardless of which runner you use day to day.
Writing tests
A Jest test file uses describe to group and test (or it) to declare a case.
// cart.test.js
import { Cart } from "./cart";
describe("Cart", () => {
test("starts empty", () => {
expect(new Cart().items).toEqual([]);
});
test("adds an item", () => {
const cart = new Cart();
cart.add({ id: 1, price: 10 });
expect(cart.items).toHaveLength(1);
});
});
Use beforeEach and afterEach for setup and teardown, and beforeAll and afterAll when a resource should be created once for the whole file. Name tests after behaviour so a failure tells you what broke.
Matchers
Jest’s expect API is large and expressive.
// matchers.test.js
expect(value).toBe(5); // strict equality
expect(value).toEqual({ a: 1 }); // deep equality
expect(value).toBeDefined();
expect(list).toContain("a");
expect(fn).toThrow("invalid");
expect(value).toBeGreaterThan(3);
expect(obj).toMatchObject({ id: 1 }); // partial match
toBe compares with Object.is and is right for primitives. toEqual recursively compares objects and arrays. toStrictEqual is stricter about types and undefined properties. toMatchObject is useful when you only care about a subset of a structure.
Mocks, spies and module mocking
Jest’s mocking is one of its strongest features.
// users.test.js
import { jest } from "@jest/globals";
import { getUser } from "./users";
jest.mock("./http", () => ({
get: jest.fn().mockResolvedValue({ id: 1, name: "Ada" }),
}));
test("returns a user", async () => {
await expect(getUser(1)).resolves.toEqual({ id: 1, name: "Ada" });
});
jest.fn creates a mock function, jest.spyOn wraps a real method so you can observe calls and restore it later, and jest.mock replaces a module. Because jest.mock is hoisted above imports, the mock is in place before the module under test loads.
Always reset mocks between tests. Enable clearMocks, resetMocks or restoreMocks in the config, or call the matching ...AllMocks helper in afterEach. Leaked mock state is one of the most common reasons tests pass alone and fail together.
Async testing
Use async/await with resolves and rejects, exactly as you would in Vitest.
// async.test.js
test("rejects on failure", async () => {
await expect(loadUser(-1)).rejects.toThrow("Invalid id");
});
The done callback works but is easy to misuse. Prefer awaiting. For timers, jest.useFakeTimers() plus jest.advanceTimersByTime() makes debounce and retry tests deterministic.
Snapshots
Snapshots serialise a value and compare it on future runs.
// config.test.js
test("builds the default config", () => {
expect(createConfig({ debug: true })).toMatchInlineSnapshot();
});
Inline snapshots live in the test file, which makes them reviewable in a diff. External snapshots are convenient for large output but are frequently updated without inspection, which turns them into a rubber stamp. Use snapshots for stable serialisable data, not as a substitute for thinking about what matters.
Configuration
Jest reads a config from jest.config.js, a jest key in package.json or a CLI flag.
// jest.config.js
export default {
testEnvironment: "jsdom",
clearMocks: true,
collectCoverage: true,
coverageThreshold: {
global: { lines: 80, functions: 80 },
},
transform: {
"^.+\\.(t|j)sx?$": ["@swc/jest"],
},
};
testEnvironment selects node or jsdom. clearMocks prevents leakage. Coverage thresholds turn a goal into an enforced rule. The transform decides how TypeScript and JSX are compiled, with ts-jest, Babel and SWC as the common options.
Where Jest fits
Jest is the unit and integration layer. For component behaviour, pair it with Testing Library to test through the user’s perspective. For full user flows, use an end-to-end tool such as Playwright or Cypress. If you are starting a new Vite project, Vitest offers the same API with a faster, shared pipeline.
Best practices
- Test behaviour through public APIs, not private internals.
- Keep one logical assertion per test where practical.
- Reset and restore mocks between tests.
- Prefer
async/awaitoverdone. - Use fake timers for anything time-dependent.
- Snapshot small, stable structures and review every update.
- Run tests in CI on every change, not only locally.
Common mistakes
- Asserting on implementation details and breaking tests during refactors.
- Blindly updating snapshots.
- Forgetting to clear mocks and leaking state across tests.
- Using real timers and making tests slow or flaky.
- Testing everything at the unit level and missing integration bugs.
- Ignoring coverage while assuming tests cover the important paths.
Where to go next
Jest is a dependable foundation and a shared vocabulary across the ecosystem. Compare it with Vitest for modern Vite projects, add Testing Library for components, and cover user journeys with Playwright or Cypress. Then write a test for a bug you recently fixed and let it protect you from regressions.