# A Beginner's Guide to Unit Testing with Jest

[Jest](https://jestjs.io/) 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](https://nodejs.org/en/download) 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:

```command
mkdir jest-demo && cd jest-demo
```

Then, initialize the directory as an npm project:

```command
npm init -y
```

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

```command
npm pkg set type="module"
```

Then, install Jest as a development dependency:  

```command
npm install --save-dev jest
```

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

```json
[label package.json]
{
  . . .
  "scripts": {
[highlight]
    "test": "node --experimental-vm-modules node_modules/.bin/jest"
[/highlight]
  }
}
```

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`:  

```javascript
[label 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:

```command
mkdir __tests__
```

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

```javascript
[label __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:  

```command
npm test
```

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

```command
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:  

```text
[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:  

```command
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:

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

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

```text
[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:

```javascript
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:

```javascript
[label __tests__/math.test.js]
describe("add function", () => {
[highlight]
  it.skip("should return 3 when adding 1 and 2", () => {
[/highlight]
    expect(add(1, 2)).toBe(3);
  });

  it("should return 5 when adding 2 and 3", () => {
    expect(add(2, 3)).toBe(5);
  });
});
```
```text
[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`:

```javascript
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:

```javascript
[label __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:

```command
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:

```command
npm test -- -t "should return 3"
```
```text
[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:

```command
npm test -- math
```

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

```command
npm test -- __tests__/math.test.js
```

### Interactive watch mode filtering

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

```command
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.

```text
[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:

```command
touch text-content.txt
```

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

```javascript
[label 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:

```javascript
[label __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:  

```text
[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.

[ad-logs]

## 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:

```javascript
[label __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", () => {
[highlight]
  beforeEach(() => {
    // Reset mock before each test
    jest.clearAllMocks();
    
    // Set up default mock behavior
    readFile.mockResolvedValue("Mocked file content");
  });
[/highlight]

  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:

```text
[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:

```javascript
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:

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

Now, you can generate a coverage report by running:

```command
npm run test:coverage
```

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

```text
[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`:

```javascript
[label __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();
[highlight]
    // remove the mock implementation for this test
[/highlight]
  });

  it("should read and return the content from a file", async () => {
[highlight]
    // Configure the mock implementation for this test
    readFile.mockResolvedValue("Mocked file content");
[/highlight]    
    // 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");
  });
[highlight]
  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");
  });
[/highlight]
});
```
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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/9722abd3-39e8-430c-3ae7-ec432e375000/lg1x =1404x1170)

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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/ac9983f6-2e1b-454c-fe4e-93704d671800/lg1x =3248x1994) 

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](https://jestjs.io/), where you'll find more in-depth information to take your testing workflow to the next level.

Happy testing! 
