Testing Next.js Applications with Jest and React Testing Library

Testing Next.js Applications with Jest and React Testing Library

Writing Bug-free and Robust Web Applications

Featured on Hashnode

In this tutorial, we will go through all the steps necessary to perform unit testing in your Next.js Applications. We are going to use React Testing Library and Jest to test our applications.

Jest is a JavaScript testing framework designed to ensure correctness of any JavaScript codebase, not necesarilly React. React Testing Library on the other hand builds on top of DOM Testing Library by adding APIs to test React Components. Jest and React Testing Library are used hand in hand for Unit Testing of React and Next.js Applications.

Getting Started

We will get started by creating a new Next.js app with Typescript support with the following command.

npx create-next-app@latest --ts

Give your project a name and open it in any code editor of you choice. Setup the content of pages/index.tsx with the code below.

import type { NextPage } from "next";
import Head from "next/head";
import styles from "../styles/Home.module.css";

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Testing Next.js</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>Testing Next.js Applications </h1>
      </main>
    </div>
  );
};

export default Home;

In the pages/index.tsx page, we are importing the NextPage component type for the current page and the default next/head component to setup the page headers. We then use the default styles that come with Next.js and setup an h1 to render a text, "Testing Next.js Applications".

Setting up Jest and React Testing Library

With the previous version of Next.js, we had to setup Jest to support Babel but the latest Next.js version uses an built-in Rust compiler with a built-in configuration for Jest so we don't need any extra configurations.

To set up Jest, install jest , @testing-library/react, @testing-library/jest-dom as development dependencies:

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

After installing the libraries, we need to setup a configuration file to use the built-in Jest configurations. Create a jest.config.js file in the root directory and add the following:

// jest.config.js
const nextJest = require("next/jest");

const createJestConfig = nextJest({
  dir: "./",
});

const customJestConfig = {
  setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
  moduleDirectories: ["node_modules", "<rootDir>/"],
  testEnvironment: "jest-environment-jsdom",
};

module.exports = createJestConfig(customJestConfig);

Check out the Next.js Docs to see what goes on under the hood in the default Jest configurations.

Next, we create a jest.setup.js file in the root directory and add the following:

import "@testing-library/jest-dom/extend-expect";

Let's add Jest to the scripts section of your package.json file to run our test in watch mode which will re-run all our tests with file changes:

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "next lint",
  "test": "jest --watch"
}

Optionally, to enable ESLint support for Jest, add the following to the .eslintrc.json file that comes default with create-next-app.

{
  "extends": "next/core-web-vitals",
  "env": {
    "jest": true
  }
}

Writing Tests

Following Jest's convention, add __tests__ folder in your project's root directory. We will store all test related files in this folder.

In writing tests, Jest offers you a describe, a test and an it global function which is used communicate with the Jest library for the various tests. The describe function is a test suite wrapper that groups related tests together. The test function is a test that is part of a suite and run individual seperate tests.

//  `describe` and `test` being used to write a test
describe("my function or component", () => {
  test("does the following", () => {
    ..
  });
});

Let's test our test runner by writing a test that checks if the Jest is setup correctly. In the __tests__ directory, create a index.test.js file, add the following:

describe("true is true and false is false", () => {
  test("true is true", () => {
    expect(true).toBe(true);
  });

  test("false is false", () => {
    expect(false).toBe(false);
  });
});

With that, we can run the test with npm run test to run the test. Correct setup of Jest will display a green check mark on the tests with this output:

 PASS  __tests__/index.test.js
  true is true and false is false
    ✓ true is true (7 ms)
    ✓ false is false (2 ms)

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

Watch Usage: Press w to show more.

Testing React Components

Now that Jest is setup, we can start writing tests for our Next.js application. To start our tests, let's create a components folder in the root directory and create a new file, components/Heading.tsx. Now, let's create a React component to render an h1 with a text as below:

import styles from "../styles/Home.module.css";

export function Heading() {
  return <h1 className={styles.title}>Testing Next.js Applications</h1>;
}

Let's import the <Heading /> component in our pages/index.tsx file replace the main tag with the code below to render the Heading component:

<main className={styles.main}>
  <Heading />
</main>

Check your browser to see if the heading is rendered correctly. Now that we have our pages/index.tsx file ready, we can start writing our tests. In the __tests__ directory, create a __tests__/components.test.tsx file with the following:

// components.test.tsx
import { render, screen } from "@testing-library/react";
import Heading from "../components/Heading";

describe("heading component", () => {
  test("renders a heading", () => {
    render(<Heading />);

    const heading = screen.getByRole("heading", {
      name: /testing next\.js applications/i,
    });

    expect(heading).toBeInTheDocument();
  });
});

What did we just do?

  • What we are testing is that the <Heading /> component renders a h1 tag with the text Testing Next.js Applications.
  • We are using the render function from @testing-library/react to render the component before we test it.
  • We are also using the screen.getByRole function to get the h1 tag that we want to test.
  • Then, we are using the expect function from Jest to test if the h1 tag is in the document.
  • Finaly, we are using the toBeInTheDocument function to test if the h1 tag is in the document.

We can now run our test and if everything is working correctly, our test will display a green check mark to show that everything worked correctly.

Testing Events

We have tested the rendering of a component. It's time to test the events that are triggered by the component. Let's start by creating a Button component. In your components/Button.tsx file and add the following :

type ButtonType = { text: string; onClick: () => void };

export default function Button(props: ButtonType) {
  return <button onClick={props.onClick}>{props.text}</button>;
}

Then we can import the Button component and add it to the pages/index.tsx file and add it to the main tag.

...
import Button from "../components/Button";
...

...
<main className={styles.main}>
  <Button text="Click Me" onClick={() => alert("Clicked!")} />
</main>
...

We can now test the new button component. In the __tests__ directory, in the __tests__/components.test.tsx file and add the following:

import Button from "../components/Button";

const defaultButtonProps = {
  onClick: jest.fn(),
  text: "Submit",
};

After importing the Button component and creating a default button props object that we can use to test the button component, we then add a describe block to test the button component. In the __tests__/components.test.tsx file and add the following to test for the button:

describe("button component", () => {
  it("renders a button", () => {
    render(<Button {...defaultButtonProps} />);

    const button = screen.getByRole("button");

    expect(button).toBeInTheDocument();
  });
});

The test above will test that the button component renders a button. The test will pass if the button is in the document and will fail if not. We can now run the test and see if the test passes.

With the button component rendered, we can test the button's onClick event. We are running a simple test to check if the button is clicked. In the __tests__/components.test.tsx file and add the following to the describe block used to test if the button component renders:

it("calls the onClick function when the button is clicked", () => {
  render(<Button {...defaultButtonProps} />);

  const button = screen.getByRole("button");

  fireEvent.click(button);

  expect(onClick).toHaveBeenCalledTimes(1);
});

What's going on here?

  • The button component has an onClick prop. The test is to check if the onClick prop, which is a function, is called when the button is clicked.
  • In doing do, we simulate a click on the button using fireEvent.click function from the React Testing Library. The provision of such function ready out of the box is why React Testing Library is the best choice.
  • Thereafter the click simulation, we use the expect function from Jest to test if the function was called and the toHaveBeenCalledTimes to check if the function was called once. You can learn more of these function from the docs of both Jest and React Testing Library.

We can run our tests from here and see if the test passes and we can move on to the next set of test.

Testing Next.js API Routes

Up next, Next.js API Routes, pun intended. Next.js provides a way to create API routes in our application and in this case, we can test the API routes. Let's create a new API then we can test it. Create a new pages/api directory and create a new [name].ts file.

Using Next.js Dynamic Routes, we want to create an API that returns a JSON object with the a name specified in the URL. For example, if we visit /api/john, we want to return a JSON object with the name property set to john.

// pages/api/[name].ts
import { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const {
    query: { name },
  } = req;

  res.statusCode = 200;
  res.setHeader("Content-Type", "application/json");
  res.json({ name: `Your name is ${name}` });
}

What are we doing here?

In our new [name].ts file, we are creating a handler that will handle all our incoming ant outgoing requests. We then destructure name from the request queries, set the status code and the content type and finally return a JSON object with the name property set to Your name is [name].

With the API route in place, we now write our test for it. In the __tests__ directory, create a new __tests__/api.test.ts file. This is where we will write the test for the API routes. Notice were doing it in a different file, we want to test components in the components file and API in the api file.

We need a library to make requests to our API route. We can use fetch since it has been added to Node.js by default but lets use an external library in case your version is lower. You can still use the default browser fetch if it is still available.Install isomorphic-fetch and @types/isomorphic-fetch to make the request to our API route with the following:

npm install --save isomorphic-fetch @types/isomorphic-fetch

In the __tests__/api.test.ts file, let's import the isomorphic-fetch package.

import fetch from "isomorphic-fetch";

We can now test our API route. In the __tests__/api.test.ts file, add the following:

describe("api routes", () => {
  it("should return the correct data", async () => {
    const data = await fetch("http://localhost:3000/api/Duncan");
    expect(data.status).toBe(200);

    const json = await data.json();
    expect(json).toEqual({ name: "Your name is Duncan" });
  });
});

What did we just do?

  • In the code above, we are testing that the API route returns the correct data using the fetch function to make the request to our API route. We are using the expect function from Jest to test if the status code of the request is 200 and we are also checking if the response we expect is indeed what we are getting from the request.

This is by far the easiest method to test Next.js API Routes, I'll cover another method in a different post. You can run your test to see if it is passing.

Conclusion

Being the last post for the #4articles4weeks challenge, I decided to write about something I love, Next.js. To recap, we are writing tests for Next.js Applications. We first created a new Next.js app with TypeScript then setup Jest and React Testing library to use for our tests. We tested for three main things; Testing the rendering of components, Testing Events on Components and Testing for API endpoints in Next.js Applications.

Thanks for your time. See you in the next post.