Back to blog

Testing Next.js Apps: Best Practices & Tools

Tech12 min read

Testing is an essential part of the software development process, ensuring that applications function as expected and remain stable over time. In this blog, we will explore the best practices for testing Next.js applications using popular testing tools like Jest, Cypress, Playwright, and React Testing Library. By following these guidelines, you can maximize your confidence in your code, catch bugs early, and deliver high-quality software to your users.

Why Testing is Important

Before we dive into the specifics of testing Next.js applications, let's understand why testing is important. Testing serves as a safeguard, helping developers catch and resolve issues before they reach production environments. It ensures that applications meet functional requirements, remain stable, and deliver a seamless user experience. Comprehensive testing can also help identify and fix bugs that may have slipped through the development process, saving time and resources in the long run.

Setting Up Next.js with Testing Tools

To get started with testing Next.js applications, we need to set up the necessary testing tools. Next.js works well with popular testing frameworks like Jest, which provides a suite of testing features such as a powerful test runner, automatic mocking, and snapshot testing. Additionally, we can leverage Cypress and Playwright for end-to-end and component testing, and React Testing Library for testing React components. These tools work seamlessly with Next.js, allowing us to write comprehensive tests for our applications.

To set up Next.js with Jest, we can use the create-next-app command and include the with-jest example. This will generate a Next.js project with the necessary configurations for Jest:

npx create-next-app@latest --example with-jest next-app

Next, we need to install the required dependencies for Jest:

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

We also need to configure Jest in the jest.config.js file. This file defines the test environment, test file patterns, and other Jest-specific configurations:

const { defaults } = require("jest-config");

module.exports = {
  ...defaults,
  testPathIgnorePatterns: ["<rootDir>/.next/", "<rootDir>/node_modules/"],
};

With Jest set up, we can now start writing tests for our Next.js application.

Writing Unit Tests with Jest

Unit tests are essential for verifying the behavior of individual components in an application. In a Next.js context, we can write unit tests for React components, API routes, and utility functions. Let's explore how to write unit tests for each of these scenarios.

Testing React Components

React components are the building blocks of our Next.js application. Writing tests for these components ensures that they render correctly, handle user interactions, and update state as expected. We can use the React Testing Library to render and interact with our components in a test environment.

Here's an example of how we can write a unit test for a simple React component:

import { render, screen } from "@testing-library/react";
import MyComponent from "./MyComponent";

describe("MyComponent", () => {
  it("renders the component", () => {
    render(<MyComponent />);
    expect(screen.getByText("Hello, World!")).toBeInTheDocument();
  });
});

In this example, we use the render function from React Testing Library to render the MyComponent component. We then use the screen object to query for the presence of a specific text content in the rendered component. If the text is found, we assert that it is in the document using the toBeInTheDocument matcher.

Testing API Routes

Next.js allows us to define API routes that handle server-side requests. These routes are an integral part of a Next.js application and may contain business logic, data fetching, and authentication. To ensure the correctness of our API routes, we can write unit tests using Jest and Supertest.

Here's an example of how we can write a unit test for an API route:

import { createRequestHandler } from "next/dist/server/api-utils";
import { createServer, Server } from "http";
import supertest from "supertest";
import handler from "./api/hello";

let server: Server;

beforeAll(() => {
  server = createServer(createRequestHandler(handler));
  server.listen();
});

afterAll((done) => {
  server.close(done);
});

describe("/api/hello", () => {
  it("returns a greeting message", async () => {
    const response = await supertest(server).get("/api/hello");
    expect(response.status).toBe(200);
    expect(response.body.message).toBe("Hello, World!");
  });
});

In this example, we create an HTTP server using the createServer function and pass our API route handler to the createRequestHandler function. We then use Supertest to send a GET request to the /api/hello endpoint and assert that the response status is 200 and the message property in the response body is equal to 'Hello, World!'.

Testing Utility Functions

Utility functions play a crucial role in any application, providing reusable logic and helper methods. Writing tests for utility functions ensures that they behave as expected and handle edge cases correctly. We can use Jest to write unit tests for our utility functions.

Here's an example of how we can write a unit test for a utility function:

import { sum } from "./utils";

describe("sum", () => {
  it("returns the sum of two numbers", () => {
    expect(sum(2, 3)).toBe(5);
  });
});

In this example, we import the sum utility function and assert that it returns the correct sum of two numbers.

Writing End-to-End Tests with Cypress

End-to-End (E2E) tests are essential for verifying the behavior of the entire application, including user interactions, navigation, and data flow. Cypress is a powerful E2E testing framework that allows us to write tests in a browser-like environment.

To set up Cypress with Next.js, we can use the create-next-app command and include the with-cypress example:

npx create-next-app@latest --example with-cypress next-app

Next, we need to install the required dependencies for Cypress:

cd next-app
npm install --save-dev cypress

We also need to update the package.json file to include the Cypress scripts:

{
    "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "cypress": "cypress open"
    }
}

With Cypress set up, we can start writing end-to-end tests for our Next.js application.

Writing End-to-End Tests

Cypress provides a powerful API for interacting with our application and asserting its behavior. We can write end-to-end tests that simulate user interactions, navigate between pages, and verify the correctness of the rendered content.

Here's an example of how we can write an end-to-end test using Cypress:

describe("Navigation", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("navigates to the about page", () => {
    cy.findByText("About").click();
    cy.url().should("include", "/about");
    cy.findByText("About Page").should("exist");
  });
});

In this example, we use the cy.visit command to navigate to the root URL of our application. We then use the cy.findByText command to locate the 'About' link and simulate a click event. After the navigation, we assert that the URL includes '/about' and that the 'About Page' text exists in the rendered content.

Cypress also provides powerful features for handling network requests, stubbing responses, and testing different scenarios. These features allow us to test our application's behavior under different conditions and ensure that it handles errors and edge cases gracefully.

Conclusion

Testing is a crucial part of the software development process, ensuring that applications function as expected and remain stable over time. In this guide, we explored the best practices for testing Next.js applications using Jest, Cypress, Playwright, and React Testing Library. We learned how to set up these testing tools, write unit tests for React components, API routes, and utility functions, and perform end-to-end tests using Cypress.

By following these guidelines, you can gain confidence in your code, catch bugs early, and deliver high-quality software to your users. Testing is an ongoing process, and as your application evolves, it's important to continually update and expand your test suite to cover new features and functionality. With a solid testing strategy in place, you can ensure the reliability and stability of your Next.js applications.

Mike van Peeren

Also in Social

Feel free to reach me at any of social networks listed below: