Next.js: A simple example including unit test


Windows WSL-based Next.js “Hello world”

Make sure that you have WSL installed and ready on your Windows machine.

Make sure that Node.js is installed in your WSL environment.

Open a WSL command line

Create a new project folder:

mkdir NextProjects

Navigate into that directory:

cd NextProjects

Install Next.js and create a project (e.g. my-next-app):

npx create-next-app my-next-app

If you have problems with yarn as explained in Errors:   yarn add –exact –cwd … react react-dom next has failed you might use the following alternative command:

npx create-next-app first-project --use-npm

After the next app is created, you can navigate into the created my-next-app folder and then enter the following command to open VS Code:

code .

If you open the folder in this way with VS Code, you are accessing your project folder in a WSL-Remote environment. You can see that in the green tab on the bottom left of the opened VS Code windows. In other words, you are accessing your VS Code running on WSL from a Windows host through remote access.

Start the application with the following command:

npm run dev

Now you can open a browser on the Windows host machine and navigate to the page http://localhost:3000 and access the application running on WSL:

Ok, granted, it’s not really a “Hello world” example but a Welcome to Next.js example.

Now you can start VS Code from within the my-next-app folder by executing the following command:

~/experiments/NextProjects/my-next-app$ code .

Changes to the code are immediately visible in the browser. For example, you can change the “Welcome to …” text in the code with something else and save your changes and immediately see your changes on the browser where the application is running.

The out-of-the-box application, which is installed automatically by using the npx create-next-app command has not only a “Welcome page” but also has a very simple example for an API: To check that API route navigate to http://localhost:3000/api/hello

And this is how a simple Next application is structured:

In our example, the Welcome page was implemented by index.js which runs on the browser (the frontend part of the Next application). And the api/hello.js is the code behind the simple API we just tested, which runs on the server side (the backend part of the Next application)

Unit Testing a Next.js application

Preparation

A testing framework often used for Next.js is the Jest testing framework.

So, let’s install it for our application.

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

Next, we have to add a config file for Jest to our project root folder:

../NextProjects/my-next-app/jest.config.js:

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

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
  moduleDirectories: ['node_modules', '<rootDir>/'],
  testEnvironment: 'jest-environment-jsdom',
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)

Add the following line to the project’s package.json file:

{
  "name": "my-next-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest --watch"
  },
  "dependencies": {
    "next": "12.3.1",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "eslint": "8.23.1",
    "eslint-config-next": "12.3.1",
    "jest": "^29.0.3"
  }
}

Make sure that jsdom is installed in your project:

npm install –save-dev jest-environment-jsdom

Now you can run the test with the following command:

npm run test

Implementation

Create a folder for unit tests under the root of the project:
For example:  ../my-next-app/__tests__/pages/
This way the structure of our unit test folder would match the structure of our pages.

Add a file index.js to the folder you just created.

Let’s implement a first empty unit test. Add the following content to the index.js file you just added:

Index.js:

import Home from "../../pages/index";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
 
 describe('Testing /pages/index', () => {
 
   it('First test case', async () => {
   });

 });

This test will for sure pass because it does nothing. You can test it like this:

npm run test

The test is passed:

Let’s implement a more meaningful unit test.

First, we add an ID to one of the HTML elements in the pages/index.js file which was created out of the box.

import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title data-testid="myTitle">Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      ...
    </div>
  )
}

Now we can extend our empty unit test with a check for that ID we just added:

import Home from "../../pages/index";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
 
 describe('Testing /pages/index', () => {
 
   it('First test case', async () => {
      render(<Home />);
      expect(screen.getByTestId("myTitle")).toBeInTheDocument();
   });

 });

And our test will pass again:

npm run test

Next.js API routes

Since Next.js allows react applications to be rendered on the server side it allows us to implement server-side functionalities such as API capabilities. This is possible with the Next.js API Routes. In other words, with the help of API Routes, we can implement an application with a frontend (react running on the browser) and a backend (API routes implemented with Next.js).

API routes are based on the folder structure ‘pages/api’. If you put a file index.js inside the folder path ‘pages/api/users/‘ then it will be accessed with the following route:

http://yourdomain/api/users

You can take the out-of-box application explained in the following section:

See Next.js: A simple example including unit test

and add a new API route to it as follows:

Create a new folder “users” under “pages/api” folder and create a file index.js inside it. Add the following content to that file:

pages/api/index.js:

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default function handleUser(req, res) {
  res.status(200).json({ name: 'Mike Dane', login: 'mdane', password: '12345' })
}

Now you have created a new API route which can be called like this:

Remark: For all this to work you don’t even have to stop and restart the server. All changes should be applied automatically.

Unit testing Next.js API routes

Here we want to examine how to unit test a Next.js API route. For example, we could try to write a unit test for the very simple API we implemented under Next.js: A simple example including unit test

Here are the necessary steps to prepare the unit test environment for Next.js:

  • Create a folder for unit tests under the root of the project:
    For example:  ../my-next-app/__tests__/pages/api/users
    This way the structure of our unit test folder would match to the structure of our API routes.
  • Add a file index.js to the folder you just created

We start with implementing an empty unit test, to be sure that it would pass, and add the following content to the index.js file:

import handleUser from "../../../../pages/api/users/";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
 
 describe('Testing /api/users', () => {
 
   it('First test case', async () => {

   });

 });

The test should pass as expected:

npm run test

Now that was just an empty unit test. Let’s try to change that unit test so that it could verify the status code 200 which is returned by the API route we want to test.

For that, we need the node-mocks-http library which would allow us to mock the HTTP communication to and from the API route endpoint. Use the following command to install it:

npm install --save-dev node-mocks-http

Now we can extend our empty unit test to test a little bit more:

import handleUser from "../../../../pages/api/users/";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import { createMocks, RequestMethod } from 'node-mocks-http';
 
 describe('Testing /api/users', () => {
 
   it('First test case', async () => {

     const { req, res } = createMocks({
       method: 'GET'
     });

     await handleUser(req, res);

     expect(res._getStatusCode()).toBe(200);
   });

 });

We prepare a mock request, which we send to the API route endpoint and then we verify the response that the API route endpoint sends back to us.

We run the test and expect a passed test because our API route does not do much beyond returning status code 200 anyway:

npm run test

More unit test topics

Passing environment variables to the unit test

In case you have to pass an environment variables to the unit test you might have the following approaches:

  • Using the .env.test.local file
  • Using assignment to process.env in the unit test

Using the .env.test.local file

No example so far.

Using assignment to process.env in the unit test

The following example shows one way we can pass environment variables from the unit test to the tested Next.js application:

Tested Next.js API:

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default function handleUser(req, res) {
  const x = process.env.VARIABLE_X;
  res.status(200).json({ varX: x })
}

The Unit Test which can set the environment value which can be read in the Next.js API and returned as part of the response:

import handleUser from "../../../../pages/api/users/";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import { createMocks, RequestMethod } from 'node-mocks-http';
 
 describe('Testing /api/users', () => {
 
   it('First test case', async () => {

     const { req, res } = createMocks({
       method: 'GET'
     });

     process.env.VARIABLE_X = "abcdef";

     await handleUser(req, res);

     expect(res._getStatusCode()).toBe(200);

     let jsonObject = res._getData();

     console.log(`variable X: ${JSON.parse(jsonObject).varX}`);
   });

 });

Unit testing a Next.js API route with external dependencies

In case you have to remove any external dependencies for test you can use the following library:

  • Install the node-mocks-http library
  • To be continued

Troubleshooting

Errors:   yarn add –exact –cwd … react react-dom next has failed

Problem

Trying to execute the following create-next-app command we get the errors further below:

npx create-next-app@latest my-next-app

Errors:

~/experiments/NextProjects$ npx create-next-app@latest my-next-app
Need to install the following packages:
  create-next-app@latest
Ok to proceed? (y) y
Creating a new Next.js app in /home/username/experiments/NextProjects/my-next-app.

Using yarn.

Installing dependencies:
- react
- react-dom
- next

Usage: yarn [options]

yarn: error: no such option: --exact

Aborting installation.
  yarn add --exact --cwd /home/username/experiments/NextProjects/my-next-app react react-dom next has failed.

And also the created folder my-next-app has only package.json file inside it and is otherwise empty!

Resolution

There seems to be a problem with yarn, you might alternatively use the npx command with the --use-npm option:

npx create-next-app my-next-app --use-npm

Error: next: not found

Problem

We have followed the instructions of Microsoft to get started with Next.js on Windows (https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nextjs-on-wsl) but at the step to run

npm run dev

we get this error:

~/experiments/NextProjects/my-next-app$ npm run dev

> my-next-app@0.1.0 dev
> next dev

sh: 1: next: not found

Resolution

The step npx create-next-app my-next-app was not successfully executed! This step has first to successfully be executed before npm run dev would work!

Error: SyntaxError: Cannot use import statement outside a module

Problem

We have a test like this:

import Home from "../pages/index";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
 
 describe('Testing /pages/index', () => {
 
   it('First test case', async () => {

   });

 });

When we run the test with

npm run test

We get the following error:

    /home/username/experiments/NextProjects/my-next-app/__tests__/pages/index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import Home from "../pages/index";
                                                                                      ^^^^^^

    SyntaxError: Cannot use import statement outside a module

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1678:14)

Potential Resolution

You might have forgotten to add the JEST configuration file (jest.config.js) to the project. Or jest.config.js is not in the root of the project but somewhere else e.g. src! Move it to the root of the project.

Error: Support for the experimental syntax ‘jsx’ isn’t currently enabled

Problem

We have a test like this:

import Home from "../pages/index";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
 
 describe('Testing /pages/index', () => {
 
   it('First test case', async () => {
      render(<Home />);
      expect(screen.getByTestId("myTitle")).toBeInTheDocument();
   });

 });

When we run the test with

npm run test

We get the following error:

    SyntaxError: /home/username/experiments/NextProjects/my-next-app/__tests__/pages/index.js: Support for the experimental syntax 'jsx' isn't currently enabled (8:14):

       6 |
       7 |    it('First test case', async () => {
    >  8 |       render(<Home />);
         |              ^
       9 |       expect(screen.getByTestId("myTitle")).toBeInTheDocument();
      10 |    });
      11 |

Potential Resolution

You might have forgotten to add the JEST configuration file (jest.config.js) to the project.

Error: Support for the experimental syntax ‘jsx’ isn’t currently enabled

Problem

We have a test like this:

import Home from "../pages/index";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
 
 describe('Testing /pages/index', () => {
 
   it('First test case', async () => {
   });

 });

And a config file like this:

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

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})

// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
  moduleDirectories: ['node_modules', '<rootDir>/'],
  testEnvironment: 'jest-environment-jsdom',
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)

When we run the test with

npm run test

We get the following error:

~/experiments/NextProjects/my-next-app$ npm run test

> my-next-app@0.1.0 test
> jest --watch

● Validation Error:

  Test environment jest-environment-jsdom cannot be found. Make sure the testEnvironment configuration option points to an existing node module.

  Configuration Documentation:
  https://jestjs.io/docs/configuration


As of Jest 28 "jest-environment-jsdom" is no longer shipped by default, make sure to install it separately.

Potential Resolution

Make sure that jsdom is installed in your project:

npm install –save-dev jest-environment-jsdom

In case you have been using yarn you might have installed jest (with yarn add –dev jest) instead of jest-environment-jsdom(with yarn add jest-environment-jsdom)

Error: ERESOLVE unable to resolve dependency tree

Problem

Trying to execute the command to install jest leads to the errors further below:

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

Error:

PS C:\myApp\myFrontpageApp> npm install --save-dev jest @testing-library/react @testing-library/jest-dom
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: myFrontpageApp@1.0.0
npm ERR! Found: react@16.14.0
npm ERR! node_modules/react
npm ERR!   react@"^16.13.1" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^18.0.0" from @testing-library/react@13.4.0
npm ERR! node_modules/@testing-library/react
npm ERR!   dev @testing-library/react@"*" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See C:\Users\j.smith\AppData\Local\npm-cache\eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\j.smith\AppData\Local\npm-cache\_logs\2022-09-22T11_38_33_740Z-debug-0.log

Potential Resolution

You might use the following yarn command instead of the failing npm command:

yarn add --dev jest

ss

Problem

Potential Resolution

Debugging

For debugging the frontend of the next.js web application you could use consle.log(‘…’) messages (you might start with console.error(‘…’) in case your log level configuration would leave console.log messages out. These log messages would appear in the browser console window.

For debugging the backend of the next.js web application, where the API calls end up, you can also use console.log(‘…’) messages which appear on the terminal from where you have started the web application e. g. with a command such as “npm run dev“.