Using React Testing Library And Jest To Test Drive A Next.js Application

Using React Testing Library And Jest To Test Drive A Next.js Application

What Is Jest?

Jest is a JavaScript testing framework that offers a set of tools to make software testing simpler. It includes a test runner that takes your code and expectations about its result, and during execution, it compares whether the result matches the expectations you had initially provided.

React Testing Library

React Testing Library is a lightweight solution for testing React components. It provides utilities for querying the DOM of React components, allowing developers to test their components in the same way actual users would.

Besides importing Jest and DOM, it also supports custom Jest matchers. The library works with React Hooks, Classes and can also be used with Regex.

Setting Up Jest With The Rust Compiler

Since the release of Next.js 12, Next.js now has a built-in configuration for Jest. To set it up, install the required packages by running the following:

yarn add -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom

Now, create a jest.config.js file in your project's root directory and add the following:

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)

Under the hood, next/jest is automatically configuring Jest for you, including:

  • Setting up transform using SWC
  • Auto-mocking stylesheets (.css, .module.css, and their scss variants) and image imports
  • Loading .env (and all variants) into process.env
  • Ignoring node_modules from test resolving and transforms
  • Ignoring .next from test resolving
  • Loading next.config.js for flags that enable SWC transforms

Add the Jest executable to the package.json scripts:

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

You can add jest --watch which will re-run tests when a file is changed

Create Your First Test

Your project is now ready to run tests.

Let’s add a Button.test.tsx file to write our first test in /components/button directory.

Button.test.tsx

import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Button from './Button';

const defaultProps = {
 onClick: jest.fn(),
 text: 'Submit',
};

describe('Test Button component', () => {
 it('renders the component', () => {
   render(<Button {...defaultProps} />);
   const button = screen.getByRole('button');
   expect(button).toBeInTheDocument();
 });
 it('renders the correct text', () => {
   const { getByRole } = render(<Button {...defaultProps} text="click me" />);
   expect(getByRole('button')).toHaveTextContent(/click me/i);
 });
 it('calls correct function on click', () => {
   const onClick = jest.fn();
   const { getByText } = render(
     <Button {...defaultProps} onClick={onClick} />,
   );
   fireEvent.click(getByText(defaultProps.text));
   expect(onClick).toHaveBeenCalled();
 });
});

You’ll first render the Button Component, get a reference to the button, the only one in this case, and expect it to be present in the document.

Note: render also returns functions that allow you to select and manipulate DOM elements. In this case, we are using getByRole, which allows you to query nodes by their role.

We also checked that by clicking on the button called the onClick prop.

Here we are creating a simple Jest mock function and passing it as the onClick prop to the Button component.

We then use getByText to select the button this time.

With the node selected, we then call fireEvent.click, which is the declarative way of RTL firing events. All we need to do next is to confirm that our mock function was indeed called.

Button.tsx

import React from 'react';

const Button = ({ onClick, text }) => {
 return <button onClick={onClick}>{text}</button>;
};

export default Button;

Let’s Add Another One

Input.tsx

import React, { useState } from 'react';

function Input() {
 const [name, setName] = useState('');
 return (
   <div>
     <span data-testid="input-greeting">
       Welcome, {name === '' ? 'Anonymous User' : name}!
     </span>
     <br />
     <input
       type="text"
       aria-label="user-name"
       placeholder="Your name"
       value={name}
       onChange={(e) => setName(e.target.value)}
     />
   </div>
 );
}

export default Input;

We have an Input component for the user’s name and a greeting message that changes with the provided input.

Note: we are using React Hooks to manage the state. However, internal implementations shouldn’t matter at all.

Let’s add an Input.test.tsx file to write our next test in /components/input directory.

Input.test.tsx

import React from 'react';
import Input from './Input';
import { render, fireEvent } from '@testing-library/react';

describe('Test Input component', () => {
 it('displays the correct greeting message', () => {
   const { getByLabelText, getByTestId } = render(<Input />);
   const input = getByLabelText('user-name');
   const greeting = getByTestId('input-greeting');
   expect(input).toHaveTextContent('');
   expect(greeting.textContent).toBe('Welcome, Anonymous User!');
   fireEvent.change(input, { target: { value: 'Ummar' } });
   expect(input).toHaveValue('Ummar');
   expect(greeting.textContent).toBe('Welcome, Ummar!');
 });
});

We select the input via the aria-label attribute—an example of how the library enforces ideal accessibility practices—and also check that both the greeting and the input have the correct initial value.

Then, we call fireEvent.change to trigger a change event, ensuring we pass the correct event object with the new input.

Finally, you’ll need to assert that the input has the correct value and the right greeting is being displayed.

  • You can access values and attributes as you would with regular DOM nodes. So getting an element’s text is as easy as accessing textContent.
  • Nodes are passed as a reference. If you assign them to a variable before a change, you can use the same variable to check if any of its attributes have changed.

Setting Up GitHub Actions

Before adding new changes to your codebase, to check for any breaking changes, you would probably want to run your tests, right?

This is where GitHub Actions come in handy.

You can use GitHub actions to automate tasks that you’d like to be repeatedly run at specific events.

What Are GitHub Actions?

GitHub Actions is a testing environment provided by GitHub. It’s a workflow file that contains YAML formatted configuration of tasks to be run when an event occurs in the repository, such as the push event. This occurs when new files are pushed to the repository.

In our repository, inside .github/workflows directory, we have the main.yml file. This is a workflow file for GitHub Actions.

It is designed to execute the yarn test command whenever a push or pull_request event occurs.

.github/workflows/main.yml

name: CI
on: [push, pull_request]
jobs:
 test:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v3
       with:
         node-version: lts
     - name: Install modules
       run: yarn
     - name: Run RTL tests
       run: yarn test

Whenever the push/pull_request event occurs, it runs the build job on an ubuntu container (server). This job first sets up Node.js version lts and then it installs all dependencies by executing the yarn command.

Finally, we’ll run the yarn test command, which executes Jest tests.

Once the workflow file is checked in and a push event occurs on the repository, GitHub Actions will start the test job mentioned in the main.yml workflow file.

image1.png If the job was successful, you would get the green tick mark. If not, you will see a red cross mark. image2.png

Conclusion

We’ve successfully learned how to successfully set up the tools and infrastructure required for a robust testing and CI/CD pipeline in a Next.js application with:

  • Unit tests for React components using Jest & React Testing Library

  • Automated Triggering of these tests occurring on each push and pull_request

We hope you found it helpful!