Frontend developer focused on inclusive design

Jest with Next.js

Testing React apps helps to ensure that each component functions correctly and as expected, leading to a reliable and bug-free application.

These are notes on using Next.js with Jest and React Testing Library for creating a React app with a robust testing environment.

Next.js

Install and sets up a new Next.js application with TypeScript support.

npx create-next-app@latest .

The code above initializes a new Next.js application in the current directory.

Here's a breakdown of what happens when you run the command:

  • npx: Executes a package using the Node Package Manager (npm) without permanently installing it.
  • create-next-app@latest: The command for creating a new Next.js application. @latest ensures that you are using the latest version of create-next-app.
  • .: This tells create-next-app to create the new project in the current directory instead of creating a new folder.

Jest

Install Jest with ts-jest to support TypeScript in React application.

npm install --save-dev jest jest-environment-jsdom ts-jest ts-node @types/jest

The code above install testing libraries for a Next.js app that uses TypeScript.

Here's what that command does:

  • npm install --save-dev: This tells npm (Node Package Manager) to install the following packages as development dependencies, meaning they will only be used during development and not in the production build of the application.
  • jest: Jest is a testing framework that works well with React and Next.js for writing and running tests.
  • jest-environment-jsdom: This is a Jest environment that simulates a web browser's environment using jsdom. This allows you to test web applications and components that rely on browser APIs as if they were running in the browser.
  • ts-jest: This is a Jest transformer that allows you to use Jest to test TypeScript code.
  • ts-node: This is used to run TypeScript code directly without precompiling, often used in testing or development scripts.
  • @types/jest: These are TypeScript declaration files for Jest, which provide type definitions for Jest functions, making it easier to use Jest in a TypeScript project with proper type checking.

Config

Initialize a default Jest configuration file with the settings necessary for ts-jest to work properly with TypeScript files.

npx ts-jest config:init

The code above creates a new file in project (often jest.config.js or jest.config.ts) with the default ts-jest settings applied.

Here's what happens when you run this command:

  • npx: Executes a package using the Node Package Manager (npm) without permanently installing it.
  • ts-jest: The package you're invoking, which is specifically designed to work with Jest to compile TypeScript test files.
  • config:init: This is an argument passed to ts-jest, instructing it to create an initial configuration file.

Initial test

Perform an initial test to verify that Next.js and Jest setup is properly configured before development.

Testing component

Create a components folder inside the src directory of Next.js project. Inside, create sum.ts file with following code:

/**
 * Sums two numbers.
 *
 * @param {number} a - The first number to add.
 * @param {number} b - The second number to add.
 * @returns {number} The sum of `a` and `b`.
 */
export function sum (a: number, b: number): number {
	// Return the sum of a and b.
  return a + b;
}

The code above is a simple function that adds two numbers together.

The sum.ts file is temporary, to know if Jest is properly executing tests and check if Jest is correctly handling TypeScript files via ts-jest.

In addition, create sum.spec.ts file with the following code:

// Import the sum function from the local sum module.
import { sum } from "./sum";

/**
 * Test suite for summing functionality.
 */
describe('sum module', () => {

  /**
   * Test case for summing two numbers.
   * It should correctly add the two numbers and return the result.
   */
  it('sums two numbers', () => {
    // Perform the sum operation with 1 and 2 as arguments.
    const result = sum(1, 2);

    // Assert that the result of the sum function is as expected (3).
    expect(result).toBe(3);
  });

  /**
   * This test case contains a deliberate TypeScript type error for testing purposes.
   * The variable 'a' is annotated as a string but is assigned a number.
   * TypeScript should catch this error, indicating the type-checking is active.
   * This test is expected to fail at compile-time due to the type mismatch.
   */
  it('checks type assertions', () => {
    // This line should cause a TypeScript error and is included to test type-checking.
    const a: string = 9;
    
    // The following assertion because the line above should fail.
    expect(a).toBe(9);
  });

});

Note that the test case checks type assertions is intentionally designed to include a TypeScript type error to perform type-checking.

Test script

Include a new script for running tests in Next.js application.

The package.json file holds various metadata relevant to the project and is used for managing the project's dependencies, scripts, and versions.

In package.json, and new test command to scripts:

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "next lint",
  "test": "jest" // This is the new test command you're adding
},

Then, run the following command in terminal:

npm run test

The command above triggers the Jest tool to start running tests.

However, the test result should fail because of the Type 'number' is not assignable to type 'string'. issue.

This problem comes from the checks type assertions check in the sum.ts file. Comment the lines to pass the test. Also, remember to remove the file once done with performing an initial test.

React Testing Library

Install React Testing Library to test components from the user's perspective

npm install --save-dev @testing-library/react @testing-library/user-event @testing-library/dom @testing-library/jest-dom

The command above installs React Testing Library and its associated packages as development dependencies in the project.

Here's what happens when you run this command:

  • npm install --save-dev: This tells npm (Node Package Manager) to install the following packages as development dependencies, meaning they will only be used during development and not in the production build of the application.
  • @testing-library/react: The core React Testing Library package provides utility functions to work with React components in your tests.
  • @testing-library/user-event: This package simulates user interactions such as clicking, typing, and more, allowing you to write tests that mimic user behavior.
  • @testing-library/dom: This package ensures that you have the latest DOM testing utilities.
  • @testing-library/jest-dom: This package offers custom Jest matchers that let you extend Jest's assertions to test the state of the DOM (e.g., whether an element is visible, has a certain attribute, etc.).

Testing component

In components folder, create a new Hello.tsx file with code for a simple React component:

/**
 * Hello component that renders "Hello World" text.
 *
 * This component is a simple stateless functional component
 * that returns a div with the text "Hello World" inside it.
 *
 * @returns {JSX.Element} The "Hello World" wrapped in a <div>.
 */
export default function Hello(): JSX.Element {
  // Return a div with "Hello World" text.
  return <div>Hello World</div>;
}

Then, create Hello.spec.tsx to test created component:

// Import the necessary functions from React Testing Library.
import { render, screen } from "@testing-library/react";
// Import the component you want to test.
import Hello from "./Hello";

/**
 * Test suite for the Hello component.
 */
describe('Hello component', () => {
  /**
   * Test case to verify if the Hello component renders "Hello World" text.
   * The expectation is that when the component is rendered,
   * it should display the text content "Hello World" within the document.
   */
  it('should render "Hello World"', () => {
    // Render the Hello component in a virtual DOM for testing.
    render(<Hello />);
    
    // Use the screen object to query for the "Hello World" text.
    const myElement = screen.getByText("Hello World");
    
    // Assert that the "Hello World" text is part of the document.
    // If the text is not found, the test will fail.
    expect(myElement).toBeInTheDocument();
  });
});

Perform test command in the terminal. Most likely, the Component test will fail because of the following possible issues:

  • SyntaxError: Unexpected token '<โ€™
  • Property 'toBeInTheDocument' does not exist

Configuration

Next.js automatically creates tsconfig.json file when selecting support for TypeScript. By default, the file uses "jsx": "preserve" in configuration which plays role in causing the issue.

To extend default configuration, create new tsconfig.jest.json file in the root of the Next.js project:

{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "jsx": "react-jsx"
    },
}

The code above specifies TypeScript configurations forJest tests, which might differ from main development or production configurations.

After, update default Jest configuration file.

Start by changing the name of Jest configuration file from jest.config.js to jest.config.ts to reflect that it is now a TypeScript file.

Second, replace the existing content ofjest.config.ts with the TypeScript code:

// Import the Config type from Jest's types to provide type-checking for the config object.
import type { Config } from '@jest/types';

// Define your Jest configuration with a typed object.
const config: Config.InitialOptions = {
  preset: "ts-jest", // Use ts-jest preset, which includes TypeScript support and jsdom environment.
  testEnvironment: "jsdom", // Specify the testing environment to simulate a browser (DOM).
  transform: {
    // Transform files matching the .ts or .tsx extension using ts-jest.
    // This allows TypeScript files to be compiled for tests.
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        tsconfig: "tsconfig.jest.json", // Specify the TypeScript config specifically for Jest.
      },
    ],
  },
  setupFilesAfterEnv: ["./src/jest.setup.ts"], // List of scripts to run after the test framework is installed in the environment.
}

// Export the configuration to be used by Jest.
export default config;

To finish configuration process, create jest.setup.ts file inside src folder:

// This import statement adds custom Jest matchers from the React Testing Library.
import '@testing-library/jest-dom';

This is a setup file for Jest that will be executed before tests. It adds @testing-library/jest-dom that extends Jestโ€™s default matchers, providing more descriptive methods for testing DOM elements.

Eslint

This step is optional but helps to find issues when writing tests using Eslint.

First, install eslint-plugin-jest package:

npm install --save-dev eslint-plugin-jest

This plugin incorporates Jest-specific linting rules for ESLint.

Second, install eslint-plugin-testing-library package:

npm install --save-dev eslint-plugin-testing-library

This plugin enforces best practices and anticipates common mistakes when writing tests with the Testing Library.

Finally, update your .eslintrc.json file with the following content:

{
  "extends": [
    "next/core-web-vitals",
    "plugin:jest/recommended",
    "plugin:jest/style",
    "plugin:testing-library/react"
  ]
}

The configuration above integrates installed plugins into ESLint configuration to apply the rules to the projectโ€™s codebase.

Here's what each line in the configuration means:

  • "next/core-web-vitals": Applies ESLint rules for Next.js projects to help optimize for Core Web Vitals.
  • "plugin:jest/recommended": Includes a set of recommended Jest rules.
  • "plugin:jest/style": Enforces style conventions for Jest tests.
  • "plugin:testing-library/react": Activates rules specific to React Testing Library.

Happy testing!