Back to Testing guides

A Beginner's Guide to Unit Testing with Jest

Stanley Ulili
Updated on February 26, 2025

Jest is a widely used JavaScript testing framework developed to provide a fast, reliable, and feature-rich testing experience.

With Jest, you get out-of-the-box support for test runners, assertions, snapshots, and mocking, making it a good choice for testing Node.js backends, React applications, and anything in between.

Its zero-config setup allows you to start writing tests immediately without additional configuration.

In this section, you'll explore Jest’s core features and learn how to write and run tests effectively.

Prerequisites

Before diving into Jest, ensure you have Node.js installed, preferably the latest LTS version.

This guide also assumes a basic understanding of JavaScript and testing principles.

Step 1 — Setting up the directory

Before you start writing tests, you will set up a proper environment for Jest. This will ensure your project is structured correctly and Jest runs smoothly with modern JavaScript features.

To begin, create a new directory and navigate into it:

 
mkdir jest-demo && cd jest-demo

Then, initialize the directory as an npm project:

 
npm init -y

This command creates a package.json file for your project configuration. Now, enable ES Modules support:

 
npm pkg set type="module"

Then, install Jest as a development dependency:

 
npm install --save-dev jest

After installing Jest, update your package.json file to include a test script:

package.json
{
  . . .
  "scripts": {
"test": "node --experimental-vm-modules node_modules/.bin/jest"
} }

This ensures Jest runs in ESM mode since Jest's ESM support is still experimental.

Next, create a simple function to test. Save the following file as math.js:

math.js
export function add(a, b) {
  return a + b;
}

The add() function takes two numbers and returns their sum.

With the function in place, you're now ready to write your first Jest test.

Step 2 — Writing your first test

Now that you have your add() function, it’s time to write a test to verify that it works as expected. Instead of manually running the function and checking results yourself, you’ll use Jest to automate this process.

First, create a directory to organize your tests. Jest commonly uses a __tests__ folder by convention:

 
mkdir __tests__

Next, create a test file for your math module. Jest recognizes files with .test.js or .spec.js extensions:

__tests__/math.test.js
import { add } from "../math.js";

describe("add function", () => {
  it("should return 3 when adding 1 and 2", () => {
    expect(add(1, 2)).toBe(3);
  });
});

In this test, you import only the add function—Jest automatically provides describe, it, and expect globally.

The describe block groups related tests for better organization, while each it function defines a specific test case with a clear description.

The expect function with the toBe matcher verifies that add(1, 2) equals 3.

Now that you have written the test, you’re ready to run it in the next step.

Step 3 — Running your tests

With your test file in place, you can now run Jest to verify that everything works as expected.

To execute all test files in your project, run the following command:

 
npm test

Since the test script in package.json is set to jest, this command is equivalent to:

 
npx jest

Jest will automatically detect and execute test files inside the tests directory. If everything is working correctly, you should see output similar to this:

Output
> jest-demo@1.0.0 test
> node --experimental-vm-modules node_modules/.bin/jest

(node:13337) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
 PASS  __tests__/math.test.js
  add function
    ✓ should return 3 when adding 1 and 2 (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.148 s, estimated 1 s
Ran all test suites.

Jest successfully detected and executed the math.test.js file inside the __tests__ directory. The output confirms that the test passed, displaying relevant details such as the number of test suites and tests executed, their pass status, and the total execution time.

Additionally, a warning about the experimental VM Modules feature is shown, along with an estimate of the expected runtime.

By default, Jest runs tests once and exits. However, you can enable watch mode for continuous testing:

 
npm test -- --watchAll

In watch mode, Jest monitors your project files and automatically reruns tests when changes are detected. This creates a rapid feedback loop that helps you identify and fix issues immediately while developing.

Step 4 — Test filtering and running specific tests

Running the entire test suite for every small change becomes inefficient as your test suite grows. Jest provides several ways to run only relevant tests, helping you maintain a fast development workflow.

Using .only to focus on specific tests

When you need to concentrate on a particular test or test group, Jest's .only modifier runs just the tests you specify, temporarily ignoring others.

For example, to focus only on the first test in your math module:

__tests__/math.test.js
describe("add function", () => {
it.only("should return 3 when adding 1 and 2", () => {
expect(add(1, 2)).toBe(3); });
it("should return 5 when adding 2 and 3", () => {
expect(add(2, 3)).toBe(5);
});
});

When you run the tests, Jest will execute only the test with .only:

Output
(node:13980) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
 PASS  __tests__/math.test.js
  add function
    ✓ should return 3 when adding 1 and 2 (2 ms)
    ○ skipped should return 5 when adding 2 and 3

Test Suites: 1 passed, 1 total
Tests:       1 skipped, 1 passed, 2 total
Snapshots:   0 total
Time:        0.387 s, estimated 1 s
Ran all test suites.
...

Notice that Jest marks the second test as skipped, letting you focus solely on the functionality you're developing or debugging.

You can also use describe.only to run all tests within a specific test suite:

 
describe.only("add function", () => {
  ...
});

describe("subtract function", () => {
  // All tests in this suite will be skipped
  ...
});

Using .skip to temporarily exclude tests

When a test is temporarily broken, or you want to exclude specific tests, Jest's .skip modifier allows you to skip them without removing or commenting out the code:

__tests__/math.test.js
describe("add function", () => {
it.skip("should return 3 when adding 1 and 2", () => {
expect(add(1, 2)).toBe(3); }); it("should return 5 when adding 2 and 3", () => { expect(add(2, 3)).toBe(5); }); });
Output
 PASS  __tests__/math.test.js
  add function
    ✓ should return 5 when adding 2 and 3 (4 ms)
    ○ skipped should return 3 when adding 1 and 2

Test Suites: 1 passed, 1 total
Tests:       1 skipped, 1 passed, 2 total
Snapshots:   0 total
Time:        0.478 s, estimated 1 s
Ran all test suites.

Watch Usage: Press w to show more.

The output confirms one skipped test, one passed test, and successful execution of all test suites.

Similarly, you can skip an entire suite with describe.skip:

 
describe("add function", () => {
  ...
});

describe.skip("subtract function", () => {
  // All tests in this suite will be skipped
  ...
});

This approach is beneficial during refactoring when specific tests might temporarily fail, but you still want to maintain your test coverage.

Filtering tests with command line arguments

Jest offers powerful command-line options to filter tests without modifying your code—ideal for CI/CD pipelines or selective testing.

Let's restore the test suite without any filtering:

__tests__/math.test.js
import { add } from "../math.js";

describe("add function", () => {
  it("should return 3 when adding 1 and 2", () => {
    expect(add(1, 2)).toBe(3);
  });

  it("should return 5 when adding 2 and 3", () => {
    expect(add(2, 3)).toBe(5);
  });
});

To run only tests containing specific text in their description, use the -t or --testNamePattern flag:

 
npm test -- -t "add function"

The double dash -- passes the subsequent arguments to Jest rather than npm. This command runs all tests with "add function" in their description.

You can further narrow your selection with more specific patterns:

 
npm test -- -t "should return 3"
Output
 PASS  __tests__/math.test.js
  add function
    ✓ should return 3 when adding 1 and 2 (1 ms)
    ○ skipped should return 5 when adding 2 and 3

Test Suites: 1 passed, 1 total
Tests:       1 skipped, 1 passed, 2 total
Snapshots:   0 total
Time:        0.147 s, estimated 1 s
Ran all test suites with tests matching "should return 3".

You can also filter by file path to run only tests in specific files:

 
npm test -- math

This runs all test files with "math" in their path. For more precise control, specify the full path:

 
npm test -- __tests__/math.test.js

Interactive watch mode filtering

Jest's watch mode provides a convenient interactive interface for filtering tests:

 
npm test -- --watchAll

In watch mode, you can access different filtering options by pressing:

  • p to filter by filename pattern
  • t to filter by test name pattern
  • f to run only failed tests
  • o to run only tests related to changed files

This menu-driven approach makes it easy to quickly focus on relevant tests during development without remembering specific command-line arguments.

Output
Watch Usage
 › Press f to run only failed tests.
 › Press o to only run tests related to changed files.
 › 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.

With these filtering capabilities, Jest lets you maintain focus on the specific parts of the codebase you're working on, significantly speeding up the development feedback loop.

Step 5 — Mocking with Jest

Mocking allows you to replace actual implementations with simulated ones, making it possible to test your code in isolation from external dependencies. This approach is useful when testing functions interacting with file systems, databases, or APIs.

In this section, you'll learn how to use Jest's mocking capabilities to test a function that reads from a file without accessing the file system.

First, create an empty text file:

 
touch text-content.txt

Next, create a fileReader.js file with a function that reads a file's contents:

fileReader.js
import { readFile } from "fs/promises";

export async function readFileContent(filePath) {
  try {
    const content = await readFile(filePath, "utf-8");
    return content.trim();
  } catch (error) {
    throw new Error(`Failed to read file: ${error.message}`);
  }
}

This function uses the Promise-based fs.readFile to asynchronously read a file's contents and return them as a trimmed string.

Now, create a test file for this function. When working with ES Modules, Jest requires a special approach for mocking:

__tests__/fileReader.test.js
import { jest } from "@jest/globals";

jest.unstable_mockModule("fs/promises", () => ({
  readFile: jest.fn(),
}));

const { readFileContent } = await import("../fileReader.js");
const { readFile } = await import("fs/promises");

describe("readFileContent function", () => {
  it("should read and return the content from a file", async () => {
    // Mock the implementation of readFile to return a specific value
    readFile.mockResolvedValue("Mocked file content");

    // Call the function with a file path
    const content = await readFileContent("text-content.txt");

    // Verify the mock was called with the correct arguments
    expect(readFile).toHaveBeenCalledTimes(1);
    expect(readFile).toHaveBeenCalledWith("text-content.txt", "utf-8");

    // Check that the function returns the mocked content
    expect(content).toBe("Mocked file content");
  });
});

Unlike CommonJS, ES Modules require a different mocking approach.

Here, jest.unstable_mockModule() replaces the real fs/promises module with a mock version before importing fileReader.js. This ensures that when readFileContent() is executed, it relies on the mocked readFile function instead of making actual file system calls.

The mock implementation of readFile returns a Promise that resolves to "Mocked file content", allowing the test to control the function’s behavior without accessing the file system.

This approach makes the test:

  • Eliminates file I/O operations.
  • Avoids issues related to file existence or permissions.
  • Always returns the expected output.

Running the test should produce output similar to:

Output
(node:15938) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
 PASS  __tests__/math.test.js
(node:15937) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
 PASS  __tests__/fileReader.test.js

Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.512 s, estimated 1 s
Ran all test suites.

Watch Usage: Press w to show more.

Jest provides several helpful assertion methods specifically for mocks:

  • toHaveBeenCalled() - verifies that a mock function was called
  • toHaveBeenCalledWith() - checks that a mock was called with specific arguments
  • toHaveBeenCalledTimes() - ensures a mock was called an exact number of times

These matchers make your tests more readable and provide better error messages when tests fail.

Step 6 — Using setup and teardown hooks

When writing multiple tests, you'll often need to perform the same setup or cleanup tasks before or after each test. Jest provides a set of powerful hooks to handle these repetitive operations, helping you maintain clean and efficient test suites.

Let's build on our file reader example and incorporate setup and teardown functionality using Jest's built-in hooks. These hooks will ensure our mocks are properly reset and maintained across multiple tests.

Update the fileReader.test.js file to include these hooks:

__tests__/fileReader.test.js
import { jest } from "@jest/globals";

jest.unstable_mockModule("fs/promises", () => ({
  readFile: jest.fn()
}));

// Import the modules after mocking
const { readFileContent } = await import("../fileReader.js");
const { readFile } = await import("fs/promises");

describe("readFileContent function", () => {
beforeEach(() => {
// Reset mock before each test
jest.clearAllMocks();
// Set up default mock behavior
readFile.mockResolvedValue("Mocked file content");
});
it("should read and return the content from a file", async () => { // Remove the readFile.mockResolvedValue("Mocked file content"); line // Call the function with a file path const content = await readFileContent("text-content.txt"); // Verify the mock was called with the correct arguments expect(readFile).toHaveBeenCalledTimes(1); expect(readFile).toHaveBeenCalledWith("text-content.txt", "utf-8"); // Check that the function returns the mocked content expect(content).toBe("Mocked file content"); }); });

The beforeEach hook executes before each test in the describe block. Here, it serves two purposes: resetting all mocks to their initial state with jest.clearAllMocks() and configuring the default behavior for our readFile mock. This ensures each test starts with a clean slate, preventing any cross-test interference.

When you run this test, you should see output similar to:

Output
(Use `node --trace-warnings ...` to show where the warning was created)
 PASS  __tests__/math.test.js
(node:17590) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
 PASS  __tests__/fileReader.test.js

Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.372 s, estimated 1 s
Ran all test suites.

Jest offers four main setups and teardown hooks, each with specific use cases:

  • beforeEach: Runs before each test, ideal for resetting state or creating fresh test data
  • afterEach: Runs after each test, perfect for cleanup operations
  • beforeAll: Runs once before all tests in a describe block, good for expensive setup operations
  • afterAll: Runs once after all tests in a describe block, useful for final cleanup

For example, when working with database connections or other expensive resources, you might prefer beforeAll and afterAll to avoid repeated setup costs:

 
describe("Database operations", () => {
  beforeAll(async () => {
    // Connect to database once for all tests
    await db.connect();
  });

  afterAll(async () => {
    // Disconnect after all tests complete
    await db.disconnect();
  });

  // Your tests here
});

These setups and teardown hooks will create more organized tests that properly isolate each test case while avoiding repetitive code.

This approach leads to a more maintainable test suite and helps ensure your tests accurately represent how your code should behave in real-world scenarios.

Step 7 — Code coverage with Jest

Code coverage helps you understand how thoroughly your code is being tested. Jest includes built-in coverage reporting capabilities that make identifying untested areas of your code and improving your testing strategy easy.

To enable coverage reporting in Jest, you only need to add the --coverage flag when running your tests.

Update the package.json file to include a dedicated script for running tests with coverage:

package.json
{
  . . .
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/.bin/jest",
"test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage"
} }

Now, you can generate a coverage report by running:

 
npm run test:coverage

This will execute your tests and produce a detailed coverage report. The output will look similar to this:

Output
(Use `node --trace-warnings ...` to show where the warning was created)
 PASS  __tests__/fileReader.test.js
 PASS  __tests__/math.test.js
---------------|---------|----------|---------|---------|-------------------
File           | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------|---------|----------|---------|---------|-------------------
All files      |      80 |      100 |     100 |      80 |                   
 fileReader.js |      75 |      100 |     100 |      75 | 8                 
 math.js       |     100 |      100 |     100 |     100 |                   
---------------|---------|----------|---------|---------|-------------------

Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.308 s, estimated 1 s
Ran all test suites.

Running your tests will generate a detailed coverage report that provides insights into how well your code is tested. The report includes several key metrics:

  • Statement coverage – Measures the percentage of executed statements.
  • Branch coverage – Tracks how many control structures, such as if statements, were tested.
  • Function coverage – Indicates the percentage of functions that were called.
  • Line coverage – Reflects the proportion of executed executable lines.

In this report, math.js achieves 100% coverage across all metrics. This means every statement, branch, function, and line in the file is tested. However, fileReader.js has only 75% line coverage, indicating that some code paths remain untested. Specifically, the tests do not cover lines 8-9, which handle error scenarios. Addressing these gaps can help improve overall test coverage and ensure robust error handling in your code.

Let's improve the coverage by adding a test for the error case in fileReader.js:

__tests__/fileReader.test.js
import { jest } from "@jest/globals";

// Mock the fs/promises module
jest.unstable_mockModule("fs/promises", () => ({
  readFile: jest.fn()
}));

// Import the modules after mocking
const { readFileContent } = await import("../fileReader.js");
const { readFile } = await import("fs/promises");

describe("readFileContent function", () => {
  beforeEach(() => {
    jest.clearAllMocks();
// remove the mock implementation for this test
}); it("should read and return the content from a file", async () => {
// Configure the mock implementation for this test
readFile.mockResolvedValue("Mocked file content");
// Call the function with a file path const content = await readFileContent("text-content.txt"); // Verify the mock was called with the correct arguments expect(readFile).toHaveBeenCalledTimes(1); expect(readFile).toHaveBeenCalledWith("text-content.txt", "utf-8"); // Check that the function returns the mocked content expect(content).toBe("Mocked file content"); });
it("should throw an error when file reading fails", async () => {
// Configure the mock to reject with an error
readFile.mockRejectedValue(new Error("File not found"));
// Verify that the function throws an error with the expected message
await expect(readFileContent("non-existent.txt")).rejects.toThrow(
"Failed to read file: File not found"
);
// Ensure the mock was called with the correct arguments
expect(readFile).toHaveBeenCalledTimes(1);
expect(readFile).toHaveBeenCalledWith("non-existent.txt", "utf-8");
});
});

The new changes modify the beforeEach hook by removing the default mock implementation of readFile.mockResolvedValue("Mocked file content"), ensuring that each test explicitly defines its own mock behavior.

The existing test is also updated to explicitly configure the mock for a successful file read, making each test more independent and preventing unintended mock behaviors from carrying over.

Additionally, a new test case is added to verify that the function correctly throws an error when file reading fails.

Running the coverage report again reflects these improvements, with all metrics reaching 100%:

Screenshot of the coverage report to show the colors

The percentage values in the report are highlighted in green, indicating that every statement, branch, function, and line of code is fully tested.

Jest's coverage reporting also generates a detailed HTML report you can view in your browser. By default, this report is saved in the coverage directory at the root of your project:

Open coverage/lcov-report/index.html in your browser to see a more interactive version of the coverage report.

Screenshot of the HTML report

With coverage reports in place, you can adopt a coverage-driven testing approach to help ensure your codebase is thoroughly tested and more resilient to changes.

Final thoughts

In this guide, you’ve explored Jest's core features and learned how to write, run, and optimize tests effectively.

You started by setting up a Jest testing environment, writing and running your first test, and then explored more advanced topics like test filtering, mocking, setup and teardown hooks, and code coverage.

To further enhance your testing skills, dive into Jest’s official documentation, where you'll find more in-depth information to take your testing workflow to the next level.

Happy testing!

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Load Testing Node.js with Artillery: A Beginner's Guide
Observability and monitoring reveals problems, but load testing with Artillery can help you find them before they happen.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github