Skip to content
FLAVIO COPES
flaviocopes.com
2026

Testing JavaScript with Jest

By

Learn how to test JavaScript with Jest, from your first test and common matchers to async tests, setup, mocks, fake timers, and snapshots.

~~~

Jest is a JavaScript testing framework maintained by the Jest contributors under the OpenJS Foundation.

It includes a test runner, assertions, mocks, code coverage, and snapshot testing. You can use it for Node.js code, browser-oriented code, React applications, and many other JavaScript projects.

Install Jest

Install Jest as a development dependency:

npm install --save-dev jest

Add a test script to package.json:

{
  "scripts": {
    "test": "jest"
  }
}

Run the tests:

npm test

Keep Jest local to the project. This makes the version reproducible for other developers and for continuous integration.

Generate a configuration file when you need one:

npm init jest@latest

Many projects can start without custom configuration.

Write your first test

Create math.js:

function sum(a, b) {
  return a + b
}

module.exports = {
  sum,
}

Then create math.test.js:

const { sum } = require('./math')

test('adds two numbers', () => {
  expect(sum(2, 3)).toBe(5)
})

Jest finds files with common names such as *.test.js and *.spec.js.

test() describes one behavior. expect() receives the actual value, and the matcher checks it.

This guide uses CommonJS to keep the setup small. ECMAScript modules and TypeScript need configuration that depends on your project. Follow Jest’s current setup guide instead of copying configuration from an unrelated stack.

Use matchers

Use toBe() for primitive values or exact object identity:

expect(2 + 2).toBe(4)

Use toEqual() to compare object or array contents:

expect({
  name: 'Flavio',
  active: true,
}).toEqual({
  name: 'Flavio',
  active: true,
})

Other useful matchers include:

Negate a matcher with not:

expect(sum(2, 3)).not.toBe(6)

Test thrown errors by passing a function to expect():

expect(() => {
  throw new Error('Invalid total')
}).toThrow('Invalid total')

Use describe() to group related behavior:

describe('sum', () => {
  test('adds positive numbers', () => {
    expect(sum(2, 3)).toBe(5)
  })

  test('adds negative numbers', () => {
    expect(sum(-2, -3)).toBe(-5)
  })
})

Groups make reports easier to read. They also give setup and teardown hooks a clear scope.

Setup and teardown

Run code before or after every test:

beforeEach(() => {
  // create fresh test data
})

afterEach(() => {
  // restore changed state
})

Run code once for the file or describe() block:

beforeAll(() => {
  // start a shared resource
})

afterAll(() => {
  // close the shared resource
})

Prefer fresh state for each test. Tests that depend on an earlier test’s side effects become difficult to run alone.

Hooks can return promises or be async.

Test asynchronous code

Make the test function async and await the result:

async function uppercase(value) {
  return value.toUpperCase()
}

test('converts text to uppercase', async () => {
  const result = await uppercase('hello')

  expect(result).toBe('HELLO')
})

Test resolved and rejected promises directly:

test('resolves with uppercase text', async () => {
  await expect(uppercase('hello')).resolves.toBe('HELLO')
})
test('rejects invalid input', async () => {
  await expect(loadUser('missing')).rejects.toThrow('User not found')
})

Always return or await the promise. Otherwise, Jest can finish the test before the assertion runs.

For a callback API, use the done argument:

test('returns a user', (done) => {
  loadUserWithCallback(42, (error, user) => {
    if (error) {
      done(error)
      return
    }

    try {
      expect(user.name).toBe('Flavio')
      done()
    } catch (error) {
      done(error)
    }
  })
})

Do not both return a promise and use done in the same test.

Create mock functions

jest.fn() creates a function that records its calls:

test('notifies the subscriber', () => {
  const subscriber = jest.fn()

  publish('New lesson', subscriber)

  expect(subscriber).toHaveBeenCalledTimes(1)
  expect(subscriber).toHaveBeenCalledWith('New lesson')
})

Give a mock a return value:

const getPrice = jest.fn().mockReturnValue(20)

expect(getPrice()).toBe(20)

Mocks are useful at a boundary such as email, a clock, or a network client. Do not mock every internal function. Tests tied to implementation details break during harmless refactoring.

Spy on an object method

jest.spyOn() wraps an existing method:

test('writes an audit message', () => {
  const logger = {
    write(message) {
      return message
    },
  }

  const spy = jest.spyOn(logger, 'write')

  logger.write('Account updated')

  expect(spy).toHaveBeenCalledWith('Account updated')

  spy.mockRestore()
})

Restore spies after the test. You can also enable Jest’s restoreMocks configuration option when every test should start with restored mocks.

Mock time

Fake timers let a test control setTimeout() and setInterval():

test('saves after one second', () => {
  jest.useFakeTimers()

  const save = jest.fn()
  setTimeout(save, 1000)

  jest.advanceTimersByTime(1000)

  expect(save).toHaveBeenCalled()

  jest.useRealTimers()
})

Use fake timers only when time is part of the behavior. Real timers are easier when a test does not need scheduling control.

Test browser code

Jest’s default environment is Node.js.

Install the jsdom environment when code needs browser DOM APIs:

npm install --save-dev jest-environment-jsdom

Then configure it:

module.exports = {
  testEnvironment: 'jsdom',
}

jsdom simulates many browser APIs. It is not a real browser, so use browser-level tests for layout, rendering, and behavior that depends on a full browser engine.

Snapshot testing

A snapshot stores a serialized value and compares it on later runs:

test('formats a user', () => {
  const user = {
    name: 'Flavio',
    role: 'author',
  }

  expect(user).toMatchInlineSnapshot(`
    {
      "name": "Flavio",
      "role": "author",
    }
  `)
})

Snapshots work best for small, stable output that reviewers can understand.

Do not update snapshots automatically just to make a failing test pass. Read the difference and confirm that the new output is intended.

For behavior that matters, a focused matcher often explains the requirement better than a large snapshot.

See the official Jest guides for getting started, matchers, asynchronous tests, mock functions, and snapshot testing.

Tagged: DevTools · All topics
~~~

Related posts about devtool: