NextJS with React Testing Library – Sample App Tutorial

In the world of web development, numerous technologies and frameworks are getting added to the stack. Among these, NextJS and React have made a good place for themselves due to their powerful features and flexibility.

In this tutorial, we will revolve around these two technologies and put focus on testing in NextJS using the React Testing Library which is an important aspect and lay stone of a successful and optimized application.

NextJS is a React-based framework that provides an efficient way to build JavaScript applications. Its server-side rendering feature has made it a popular choice among developers.

The React Testing Library is a lightweight solution for testing React components. It provides a simple and complete testing solution that works out of the box for your React applications.

Before we dive in, there are some prerequisites that you should be familiar with. These include basic knowledge of JavaScript, ReactJS, and NextJS.

 

Setting Up the Development Environment

The first step involves setting up the development environment. Install Node.js and npm on your system if you haven’t already.

Create a new NextJS application using the command:

npx create-next-app@latest.

 

Building the Application

Let’s start with a basic one-page app that displays a welcoming message to the user. Here’s how you can create it:

// pages/index.js

import React from 'react';

export default function Home() {
  return <h1>Welcome to NextJS!</h1>;
}

This is a simple NextJS application where the main page renders a welcoming message.

 

Installation and Setup

Let’s have a look at various libraries which are required to have an environment ready to create our tests and also run those tests successfully:

React Testing Library

To use the React Testing Library, we need to install the library first. Execute the command npm install --save @testing-library/reactto install the package and make it available for your application.

 

Install & Configure Jest

If you have only installed @testing-library/react, you’ll still need to install jest as your test runner, along with a few more dependencies, to set up your testing environment.

Here are the steps to get started:

1 – Install Jest:

Jest is a JavaScript testing framework developed by Facebook, and it’s widely used for testing React applications. To install Jest, run the following command in your terminal:

npm install --save-dev jest

2 – Install Babel:

Babel is a tool that transpires modern JavaScript (ES6+) into a backwards compatible version of JavaScript that can be run in older environments. Since Jest runs in Node, and Node doesn’t support some ES6 features (like import statements), we need Babel to transpile our tests. Install Babel and the necessary presets with the following command:

npm install --save-dev @babel/preset-env @babel/preset-react babel-jest

 

Then, create a .babelrc file in your project root with the following content:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

 

3 – Set up Jest with React Testing Library:

Now, you need to configure Jest to work with the React Testing Library. To do this, install @testing-library/jest-dom which provides custom jest matches that you can use to extend jest. These will make your tests more declarative, clear to read and to maintain.

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

 

Then, in your project root, create a file named setupTests.js (or setupTests.ts if you’re using TypeScript), and add the following line:

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

This will add the custom jest matches from @testing-library/jest-dom to Jest.

 

4 – Configure Jest:

Lastly, you need to add some configuration for Jest. In your package.json, add the following:

"jest": {
  "setupFilesAfterEnv": ["<rootDir>/setupTests.js"],
  "testPathIgnorePatterns": ["<rootDir>/.next/", "<rootDir>/node_modules/"]
}

This tells Jest to run setupTests.js before running your tests, and to ignore any test files in your .next and node_modules directories.

 

Writing First and Most Simple Test

Once installed, we can start writing tests. Consider a simple test where we check if the greeting message renders correctly.

// tests/index.test.js

import { render, screen } from '@testing-library/react';
import Home from '../pages/index';

describe('Home', () => {
  it('renders a welcome message', () => {
    render(<Home />);
    expect(screen.getByText('Welcome to NextJS!')).toBeInTheDocument();
  });
});

This test checks if the “Welcome to NextJS!” text is present in the document. If it’s there, the test passes; if it’s not, the test fails.

 

How to Run the Test?

You can easily run tests for your Next.js application with Jest and React Testing Library. First, make sure that Jest is correctly set up in your project.

If you’ve followed the previous sections of this article, you should have already installed Jest and the necessary dependencies using either npm or yarn.

With Jest installed, you will typically add a script to your package.json file to make running your test suite easier. Here’s an example:

// package.json

{
  "scripts": {
    "test": "jest"
  }
}

In this configuration, the test script will run Jest, which will automatically find and run all test files in your project. By convention, Jest looks for test files with either a .spec.js or .test.js extension, or files inside a __tests__ directory.

To run your test suite, open your terminal and navigate to your project directory. Then, run the test script with npm or yarn:

npm test
# or
yarn test

This command will start Jest, and it will run all the tests in your project. If the tests pass, Jest will print a success message and a summary of the test results. If any tests fail, Jest will print an error message and details about the failed tests.

 

Dealing with Asynchronous Code

When testing asynchronous code, we might run into some difficulties. Nevertheless, React Testing Library provides utilities such as waitFor and findBy that make it easier to handle asynchronous operations.

For instance, if our application fetches data from an API and displays it, we can use findBy to wait for the data to be rendered:

// tests/index.test.js

import { render, screen } from '@testing-library/react';
import Home from '../pages/index';

describe('Home', () => {
  it('renders fetched data', async () => {
    render(<Home />);
    const fetchedData = await screen.findByText(/fetched data/i);
    expect(fetchedData).toBeInTheDocument();
  });
});

 

Why is Asynchronous Code More Challenging to Test?

Asynchronous code can be more challenging to test due to its nature. Unlike synchronous code, which executes in the order it’s written, asynchronous code doesn’t necessarily execute in a linear sequence. Instead, it may start executing now but finish later, introducing a time element that can complicate testing.

Let’s consider a simple component in our NextJS application that fetches user data from an API and displays it:

// pages/User.js

import React, { useEffect, useState } from 'react';

const User = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch('/api/user')
      .then((response) => response.json())
      .then((data) => setUser(data));
  }, []);

  if (!user) {
    return <div>Loading...</div>;
  }

  return <div>{user.name}'s data loaded successfully!</div>;
};

export default User;

This component uses the fetch API to get user data when the component mounts, and then sets this data into the state. As long as the data isn’t fetched, it shows a “Loading…” message.

Now, if we were to test this component, we might write a test like this:

// tests/User.test.js

import { render, screen } from '@testing-library/react';
import User from '../pages/User';

describe('User', () => {
  it('displays the user data after it has been fetched', () => {
    render(<User />);
    const userData = screen.getByText(/'s data loaded successfully!/i);
    expect(userData).toBeInTheDocument();
  });
});

But this test will fail. Why? Because getByText will try to find the text immediately, but the data hasn’t been fetched yet. The data fetching is an asynchronous operation – it takes some time to complete. And by the time the data is fetched, getByText has already executed and didn’t find the text.

This is where the React Testing Library’s asynchronous utilities come into play. We can use findByText, which returns a Promise that resolves when the element with the given text is added to the DOM:

// tests/User.test.js

import { render, screen } from '@testing-library/react';
import User from '../pages/User';

describe('User', () => {
  it('displays the user data after it has been fetched', async () => {
    render(<User />);
    const userData = await screen.findByText(/'s data loaded successfully!/i);
    expect(userData).toBeInTheDocument();
  });
});

Now, the test will wait for the data to be fetched and added to the DOM, and then it will check if the text is present. This ensures our asynchronous code is tested correctly.

 

Testing Components with Router

Components that use Next.js router can sometimes present a testing challenge. Let’s consider a simple Next.js application that uses a router.

For instance, we have a UserPage component that displays the user’s ID, which it gets from the URL:

// pages/UserPage.js

import { useRouter } from 'next/router';

const UserPage = () => {
  const router = useRouter();
  const { id } = router.query;

  return <div>User ID: {id}</div>;
};

export default UserPage;

This component uses Next.js’s useRouter hook to access the router and get the user ID from the URL. Now, if we were to test this component, we would need to make sure that the router’s query includes the correct ID.

However, when rendering the component in a test environment, there is no actual router or URL, so router.query would be undefined, causing the test to fail.

To solve this issue, we can use a custom render function that wraps the component with a RouterContext.Provider and sets the value of the router manually.

Here’s an example of how to do it:

// tests/test-utils.js

import { render } from '@testing-library/react';
import { RouterContext } from 'next/dist/shared/lib/router-context';

const customRender = (ui, { router = {}, ...options } = {}) => {
  const mockedRouter = {
    basePath: '',
    pathname: '/',
    route: '/',
    asPath: '/',
    query: {},
    push: jest.fn().mockResolvedValue(true),
    replace: jest.fn().mockResolvedValue(true),
    reload: jest.fn(),
    back: jest.fn(),
    prefetch: jest.fn().mockResolvedValue(undefined),
    beforePopState: jest.fn(),
    events: {
      on: jest.fn(),
      off: jest.fn(),
      emit: jest.fn(),
    },
    isFallback: false,
    ...router,
  };

  return render(<RouterContext.Provider value={mockedRouter}>{ui}</RouterContext.Provider>, options);
};

export * from '@testing-library/react';
export { customRender as render };

This customRender function takes the UI to render and an options object. It uses a mocked router with default values, but you can override these values by passing a router object in the options. Then, it renders the UI within a RouterContext.Provider that uses this mocked router.

Now, we can use this customRender function to test our UserPage component:

// tests/UserPage.test.js

import { screen } from './test-utils';
import UserPage from '../pages/UserPage';

describe('UserPage', () => {
  it('displays the user ID from the router', () => {
    customRender(<UserPage />, { 
      router: { query: { id: '1' } }, 
    });
    expect(screen.getByText('User ID: 1')).toBeInTheDocument();
  });
});

This test renders the UserPage component and sets the router’s query to { id: '1' }. Now, when the UserPage component tries to access router.query, it gets { id: '1' }, and the test passes.

 

Using Custom Render

A custom render function can enhance the testing process by wrapping components with context providers or decorators. This becomes especially handy when testing components that rely on contexts, like theme or localization.

A custom render function can also wrap components with providers like Redux’s Provider or Apollo’s ApolloProvider. This allows for a more realistic testing environment that mirrors your actual application’s structure.

Here’s an example of how you could create a custom render function that wraps components with a theme provider:

// tests/test-utils.js

import { render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';

const theme = {
  colors: {
    primary: '#0070f3',
  },
};

const customRender = (ui, options) =>
  render(ui, { wrapper: ({ children }) => <ThemeProvider theme={theme}>{children}</ThemeProvider>, ...options });

// re-export everything from RTL
export * from '@testing-library/react';

// override the built-in render with our own
export { customRender as render };

In this example, the customRender function takes the same arguments as React Testing Library’s render function. It then calls render, passing a wrapper option. This wrapper is a component that will wrap the component you’re testing. In this case, it’s a ThemeProvider that provides a theme to the component.

 

Now, when you want to test a component that uses the theme context, you can use this customRender function:

// tests/ThemedComponent.test.js

import { screen } from './test-utils';
import ThemedComponent from '../components/ThemedComponent';

describe('ThemedComponent', () => {
  it('uses the correct primary color from the theme', () => {
    render(<ThemedComponent />);
    expect(screen.getByText('Primary color is #0070f3')).toBeInTheDocument();
  });
});

By using a custom render function, you can make sure that your components have access to all the contexts they need, which can lead to more accurate and useful tests.

 

Debugging Tests

When tests fail or behave unexpectedly, debugging becomes a crucial part of the process. Fortunately, React Testing Library provides some useful utilities for debugging your tests.

One of the most useful utilities for debugging is the screen.debug() function. This function prints the entire rendered component tree to the console, which can help you understand what’s going on in your tests.

For example, if you have a test that’s failing and you’re not sure why, you could use screen.debug() like this:

// tests/Component.test.js

import { render, screen } from '@testing-library/react';
import Component from '../Component';

describe('Component', () => {
  it('renders the text', () => {
    render(<Component />);
    screen.debug();
    expect(screen.getByText('Hello, World!')).toBeInTheDocument();
  });
});

When this test runs, it will print the rendered component tree to the console before trying to find the “Hello, World!” text. This can help you see if the component is rendering correctly or if there’s something unexpected in the component tree.

If you’re looking for a specific part of the component tree, you can pass a DOM node to screen.debug(). This will print only that part of the component tree. For example:

// tests/Component.test.js

import { render, screen } from '@testing-library/react';
import Component from '../Component';

describe('Component', () => {
  it('renders the text', () => {
    render(<Component />);
    const element = screen.getByText('Hello, World!');
    screen.debug(element);
    expect(element).toBeInTheDocument();
  });
});

In this case, screen.debug(element) will print only the part of the component tree that includes the “Hello, World!” text.

Remember, debugging is a powerful tool in the testing process. Being familiar with debugging utilities like screen.debug() can make your testing life much easier.

 

Conclusion

Testing is a critical part of any application development process. When it comes to React applications built with Next.js, the React Testing Library provides a robust and user-centric approach to ensure that your components work as expected.

In this article, we delved into the basics of setting up the React Testing Library with Next.js and Jest. We further explored how to write tests for different scenarios including asynchronous code, components using routers, and even custom render functions to enhance your testing process.

Debugging tests is equally essential as writing them, and we discussed how to utilize the screen.debug() function for this purpose.

With this guide at your disposal, you’re now better equipped to write and debug tests, contributing to the overall quality and reliability of your Next.js applications.

 

Leave a Comment

Your email address will not be published. Required fields are marked *