Testing with Jest
Jest is the most widely-used JavaScript testing framework — an all-in-one toolkit with a test runner, an expect assertion library, built-in mocking, snapshot testing, and coverage, all configured to work together out of the box. It became the default for React apps and remains a top choice for Node backends. This page covers setup, the expect matchers, mocking with jest.fn() and jest.mock(), async testing, and the ESM caveat that trips people up.
Setup
Bash
npm install --save-dev jest # For ES modules / TypeScript: npm install --save-dev jest @types/jest ts-jest
package.json
JSON
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}jest.config.js
JS
export default {
testEnvironment: 'node', // 'node' for backends, 'jsdom' for browser code
coverageThreshold: {
global: { branches: 80, functions: 80, lines: 80, statements: 80 },
},
testMatch: ['**/*.test.js'],
}The expect API
JS
import { calculateTotal } from './cart.js'
describe('calculateTotal', () => {
test('sums item prices', () => {
expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15)
})
test('returns an object with the right shape', () => {
expect(buildCart()).toEqual({ items: [], total: 0 }) // deep equality
})
test('various matchers', () => {
expect(value).toBeTruthy()
expect(arr).toContain('apple')
expect(arr).toHaveLength(3)
expect(obj).toHaveProperty('user.name', 'Ada')
expect(fn).toThrow(/invalid/)
expect(num).toBeGreaterThan(10)
expect(str).toMatch(/^https:/)
expect(promise).resolves.toBe(42) // async resolve
})
})`toBe` is reference/primitive equality; `toEqual` is deep structural equality
A frequent beginner mistake: using `toBe` to compare objects or arrays. `toBe` uses `Object.is` (reference equality), so `expect({a:1}).toBe({a:1})` **fails** — they're different references. Use `toEqual` for deep structural comparison of objects/arrays, and `toBe` for primitives (numbers, strings, booleans) and checking that two variables reference the same object. `toStrictEqual` additionally checks for `undefined` properties and class types.
Setup and teardown hooks
JS
describe('UserRepository', () => {
let db
beforeAll(async () => { db = await connectTestDb() }) // once, before all tests
afterAll(async () => { await db.close() }) // once, after all
beforeEach(async () => { await db.users.deleteAll() }) // before EACH test
afterEach(() => { jest.clearAllMocks() }) // reset mocks between tests
test('saves and retrieves a user', async () => {
await db.users.create({ email: 'a@x.com' })
const found = await db.users.findByEmail('a@x.com')
expect(found).not.toBeNull()
})
})Mocking with jest.fn() and jest.mock()
JS
// 1. A mock function:
const sendEmail = jest.fn().mockResolvedValue({ id: 'msg_1' })
await notifyUser(sendEmail, 'a@x.com')
expect(sendEmail).toHaveBeenCalledWith('a@x.com')
expect(sendEmail).toHaveBeenCalledTimes(1)
// 2. Auto-mock an entire module:
jest.mock('./emailService.js')
import { sendEmail as mockedSend } from './emailService.js'
mockedSend.mockResolvedValue({ id: 'msg_1' })
// 3. Spy on a real method (keeps original unless you mockImplementation):
const spy = jest.spyOn(console, 'error').mockImplementation(() => {})
// ...assert it was called, original suppressed...
spy.mockRestore()`jest.mock()` is hoisted — it replaces the module before any import runs
`jest.mock('./module')` is automatically **hoisted** to the top of the file by Jest's Babel transform, so the mock is in place before the module is imported anywhere. This is why you can write `jest.mock()` after your imports and it still works — but it's also why you can't reference outside variables inside the mock factory (they don't exist yet at hoist time). [Mocking](/nodejs/mocking) covers the patterns and pitfalls in detail.
Async testing
JS
// async/await — the cleanest:
test('fetches a user', async () => {
const user = await getUser(1)
expect(user.name).toBe('Ada')
})
// Testing rejection:
test('throws for a missing user', async () => {
await expect(getUser(999)).rejects.toThrow('not found')
})
// resolves/rejects matchers:
test('resolves to the user', async () => {
await expect(getUser(1)).resolves.toHaveProperty('id', 1)
})Always `await` or `return` async assertions — a forgotten await makes the test pass silently
If you write `expect(getUser(999)).rejects.toThrow()` without `await` (or `return`), Jest finishes the test before the promise settles, and the assertion never runs — the test passes even when the behaviour is wrong. Always `await` (or `return`) any `expect().resolves`/`expect().rejects` and any promise inside a test. ESLint's `jest/valid-expect-in-promise` and `jest/no-conditional-expect` rules catch many of these.
Snapshot testing
JS
test('serializes a user to the expected shape', () => {
const dto = toUserDto({ id: 1, email: 'a@x.com', passwordHash: 'secret' })
expect(dto).toMatchSnapshot()
// First run: writes __snapshots__/file.test.js.snap
// Later runs: fails if the output changed (review + 'jest -u' to update)
})Snapshots are easy to abuse — review them and don't blindly update with `-u`
Snapshot tests capture a value on first run and fail when it changes. They're great for catching unintended changes to serialized output (DTOs, rendered components). The danger: when a snapshot fails, it's tempting to run `jest -u` to "fix" it without examining what changed — which defeats the purpose. Review snapshot diffs as carefully as code. Keep snapshots small and focused; a giant snapshot of an entire response is hard to review and breaks on every trivial change.
The ESM caveat
Jest's ESM support requires extra flags — many teams use ts-jest or stay on CommonJS
Jest was built around CommonJS, and its ES-module support is still behind an experimental flag (`node --experimental-vm-modules`). If your project is pure ESM, you may hit friction with `jest.mock()` hoisting and dynamic imports. Options: use `ts-jest` or `babel-jest` to transpile, run with the experimental VM modules flag, or consider [Vitest](/nodejs/jest) (a Jest-compatible runner with native ESM) or the [built-in node:test runner](/nodejs/node-test-runner) which handles ESM natively. For new ESM projects, evaluate these before committing to Jest.
Next
The flexible, composable classic: [Mocha & Chai](/nodejs/mocha-chai).