Mastering Jest Config for your JavaScript projects

Welcome to JavaScript testing with Jest! Let's understand how to configure Jest effectively to make your dev and testing experience much smoother.

In this post, we'll cover the essentials of the Jest config, set up a project from scratch, and write a test case that showcases some powerful features of Jest.

Why choose Jest for your JavaScript testing

Jest is an awesome JavaScript testing framework with a focus on simplicity.

It works out of the box for most JavaScript projects, providing an integrated testing solution that helps developers write and run tests with ease.

Its zero-config philosophy means you can get started quickly, but it also offers powerful configuration options for more complex needs.

Setting up a new JavaScript project

Before we get our hands on the Jest config, let's set up a new JavaScript project. Start by creating a new directory for your project and navigate into it.

Then, run npm init -y to generate a package.json file with default values. This file will be the heart of your project's configuration, including your Jest setup.

We'll be using JavaScript modules, so don't forget to edit your package.json file to specify "module" for the type setting like so:

Initializing Jest with sensible defaults

Now, it's time to introduce Jest into your project. Run npx jest --init to create a default Jest configuration file.

You'll be prompted with a few questions about how you'd like to set up your testing environment. Choose the options that best suit your project's needs, but this is how I usually set up my projects:

Once you've initialized Jest, you can start customizing the Jest config file. Here's a configuration that I find works well for most projects:

/*
 * For a detailed explanation regarding each configuration property, visit:
 * https://jestjs.io/docs/en/configuration.html
 */

export default {
  // Automatically clear mock calls and instances between every test
  clearMocks: true,

  // The directory where Jest should output its coverage files
  coverageDirectory: "coverage",

  // Indicates which provider should be used to instrument code for coverage
  coverageProvider: "v8",

  // A list of reporter names that Jest uses when writing coverage reports
  coverageReporters: ["json", "text", "lcov", "clover"],

  // An object that configures minimum threshold enforcement for coverage results
  coverageThreshold: {
    global: {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100,
    },
  },

  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
  maxWorkers: "50%",

  // The test environment that will be used for testing
  testEnvironment: "node",
};

This setup clears mocks between tests, specifies where to output coverage reports and sets high standards for coverage thresholds. It's a robust starting point that encourages quality in your test suite, but feel free to modify them however you like.

Modifying package.json for efficient testing

To streamline your testing workflow, modify your package.json to include some handy scripts:

"scripts": {
  "dev": "nodemon src/index.js",
  "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --runInBand --watchAll --config ./jest.config.js",
  "coverage": "NODE_OPTIONS=--experimental-vm-modules npx jest --config ./jest.config.js --coverage"
},

These scripts provide quick commands to start your development server, run tests, and generate coverage reports.

Important: you're gonna need to have Nodemon installed globally or in your project. Use npm i -g nodemon to make it globally available or npm i nodemon to install it just for your current project.

Writing your first test with Jest

Let's put our Jest config to work by writing a test case.

We'll begin by creating our main module at /src/index.js:

// /src/index.js
export function add(a, b) {
  return a + b;
}

And then a corresponding test file /test/index.test.js:

// /test/index.test.js
import { beforeEach, describe, expect, jest, test } from "@jest/globals";
import { add } from "../src/index";

// Mock function to spy on the actual implementation
const mockAdd = jest.fn(add);

// This will run before each test within this describe block
beforeEach(() => {
  // Clear all information stored in the mock function
  mockAdd.mockClear();
});

describe('add function', () => {
  test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
    // Directly calling the mock function to check if it was called correctly
    mockAdd(1, 2);
    expect(mockAdd).toHaveBeenCalledWith(1, 2);
    expect(mockAdd).toHaveReturnedWith(3);
  });

  test('adds 5 + 7 to equal 12', () => {
    expect(add(5, 7)).toBe(12);
    // Directly calling the mock function to check if it was called correctly
    mockAdd(5, 7);
    expect(mockAdd).toHaveBeenCalledWith(5, 7);
    expect(mockAdd).toHaveReturnedWith(12);
  });

  // Using the jest mock to spy on the function call
  test('mock function is called with the right arguments', () => {
    const a = 10;
    const b = 20;
    mockAdd(a, b);
    expect(mockAdd).toHaveBeenCalledTimes(1);
    expect(mockAdd).toHaveBeenCalledWith(a, b);
  });
});

In this test file:

  • beforeEach is used to reset the mock function's state before each test, ensuring no test interferes with another.

  • describe creates a block that groups together several related tests for the add function.

  • test contains the individual test cases.

  • expect makes assertions about what is expected. In this case, it's used to assert that the add function returns the correct value and that the mock function is called with the expected arguments.

  • jest.fn() creates a mock function that we use to spy on the calls to the add function. This allows us to ensure that our add function is called correctly without actually invoking the real implementation during the test.

This simple test suite demonstrates the basic usage of Jest's global functions and how they can be combined to write effective unit tests for JavaScript functions.

Running Your Tests Continuously with the --watchAll Flag

The next step is to run the tests and see the results!

With Jest, you can run your tests in watch mode using the --watchAll flag. This powerful feature allows Jest to automatically re-run your tests when it detects changes in your code or tests, providing instant feedback on your changes.

By including the --watchAll flag in our package.json scripts, as shown earlier, you can start Jest in watch mode simply by running npm test.

This mode is particularly useful during development when you are making frequent changes to your code. Instead of manually re-running your tests after every change, Jest streamlines your workflow by watching for file changes and re-running the relevant tests for you.

This continuous feedback loop helps you to stay focused on writing code, as you can write a test, implement the functionality, and immediately see if your changes have passed the test or if you need to make further adjustments. It's an essential part of the test-driven development (TDD) process, encouraging you to work in small increments and continuously validate your work.

Remember, the --watchAll flag is intended for use during development. For a single run of your test suite, such as in a continuous integration (CI) environment, you would run Jest without this flag to ensure all tests are executed only once.

Leveraging Jest features for robust tests

Jest provides a rich set of features to make your tests robust and maintainable. Let's have a closer look on the features that we used in our tests:

Using beforeEach for test setup

The beforeEach function is used to set up any conditions that are necessary before each test runs. This is a great place to reset mocks or create consistent testing states.

Describing test suites with describe

The describe block helps you group related tests together, making your test files easier to read and manage.

Assertions with expect for test validations

expect is where you'll make assertions about the state of your application. It's how you'll test if your code is behaving as expected.

Mocking with jest for behavior verification

Jest's mocking capabilities are powerful. You can create spies on functions to ensure they are called with the correct arguments, or mock entire modules to control your testing environment fully.

Writing test cases with test

The test function is where you'll write the individual test cases. Each test should focus on a small piece of functionality and should be able to run independently of other tests.

Implementing a Spy in Jest

To demonstrate a spy in action, let's say you have a function that calls another function. You can use jest.spyOn() to verify that the second function is called correctly.

jest.spyOn(module, 'functionToSpyOn');
expect(module.functionToSpyOn).toHaveBeenCalledWith(expectedArgs);

This ensures that your functions interact as expected, which is a critical aspect of unit testing.

Ensuring Code Quality with Test Coverage

In any testing strategy, one of the key aspects to monitor is test coverage.

Test coverage is a metric used to measure the amount of code that is covered by your tests, which can be crucial for maintaining high-quality code.

In the Jest configuration we've set up, we've included a coverageThreshold that enforces 100% coverage on branches, functions, lines, and statements. This means that for your tests to pass, they must cover every part of your code.

Feel free to adjust the coverage threshold to whatever makes sense in your setting.

Running Test Coverage with npm

To generate a coverage report, you can use the npm run coverage command that we've added to the package.json scripts.

This command is a shortcut for running Jest with the --coverage flag, which tells Jest to collect coverage information and generate a report:

"scripts": {
  "coverage": "NODE_OPTIONS=--experimental-vm-modules npx jest --config ./jest.config.js --coverage"
},

When you run npm run coverage, Jest will execute your test suite and collect data on how much of your codebase is covered by your tests. After the tests complete, Jest will output a coverage report in the terminal.

Additionally, it will create a coverage directory with detailed HTML reports that you can open in a web browser to see exactly which lines of code are not covered by tests.

The importance of a high test coverage

Setting high standards for test coverage ensures that you write enough tests to catch potential bugs and maintain a healthy codebase.

However, it's important to note that while high test coverage can significantly reduce the chances of bugs, it does not guarantee that your code is bug-free. Test coverage should be used as a guide to identify untested parts of your code, not as the sole indicator of code quality.

By running npm run coverage, you make it a regular part of your development process to check and ensure that your code meets the high standards you've set. This practice can help you catch issues early and facilitate a culture of quality in your development workflow.

Exploring the coverage report in your browser

After running your test coverage command with npm run coverage, Jest creates a coverage folder in your project directory.

To delve into the details of your test coverage, navigate to the /coverage/lcov-report/index.html file. Open this file in your web browser, and you'll be greeted with a comprehensive coverage report.

The coverage report visually represents which lines of code were executed during your tests and which were not, using color-coded annotations.

Lines that are covered are highlighted in green, indicating that your tests are effectively exercising these parts of your code.

Any lines not covered are marked in red, drawing immediate attention to potential areas where you might want to write additional tests. Let's see this in action by adding a new function to our index.js file:

// /src/index.js
export function add(a, b) {
  return a + b;
}

// Add this function to your code.
// We'll leave it untested on purpose.
export function newUntestedFunction() {
  return true;
}

Now, generate a new coverage report with npm run coverage:

Also, let's have a look at the HTML version:

Closing thoughts

By now, you should have a solid understanding of setting up a Jest config and writing tests that help maintain a high-quality codebase.

Remember, a good Jest config sets the stage for a robust testing environment, but it's the quality of your tests that ultimately ensures your code is reliable.

Keep practicing, stay curious, and embrace test-driven development to make your JavaScript projects more maintainable and error-free. Happy testing!

Wanna see it all in a real project?

I've created a series of posts solving the RPG Combat Kata code challenge, applying everything you've seen in this post. Check it out!