DEV Community

Cover image for Testing Next.js components using Cypress
Emmanuel Ugwu
Emmanuel Ugwu

Posted on

Testing Next.js components using Cypress

The popularity of JavaScript frameworks is a testament to the flexibility they offer developers, allowing them to build complex applications. An example is Next.js, a React-based framework with pre-rendering, automatic code-splitting, and greater application scaling flexibility.

Every developer's goal is to build software that works. With a good JavaScript framework and adequate testing techniques, a developer can build software that works flawlessly. Testing enables developers to write efficient code, which saves them hours of debugging and eventually reflects in overall software quality.

Cypress is an end-to-end testing framework that creates tests for modern web applications, debugs them visually, and automatically runs them in continuous integration builds.

What we’ll build

This post reveals the similarities and differences between Cypress testing techniques and how to run tests in a Next.js application. The complete source code of this project is on GitHub. Fork it to get started quickly.

Prerequisite

A basic understanding of React.js and test UIs is needed to get the most out of this article.

Getting started with Cypress

First, you'll bootstrap a new Next.js app by running the following command in your terminal:

npx create-next-app <project-name>
Enter fullscreen mode Exit fullscreen mode

Navigate into the project directory and install Cypress locally as a dev dependency.

#navigate to the newly created app
cd <project-name>

#install cypress
npm install cypress --save-dev

#run cypress via npm
npx cypress open
Enter fullscreen mode Exit fullscreen mode

NOTE: <project-name> in the code snippet above stands for the app's name; you can call it whatever you want.

Once this is done, you'll need to include a way to launch Cypress from the terminal. You'll add a Cypress command to the scripts field in your package.json file to do this.

{
"scripts": {
    "cypress:open": "cypress open"
  }
}
Enter fullscreen mode Exit fullscreen mode

Invoking the command npx cypress open from your project root starts the Cypress Launchpad.

Cypress Launchpad

Before you start writing your first test, you'll have to choose what test your application requires. Cypress offers two options: End-to-End and Component Testing.

Testing Types

Each option has its advantages and disadvantages, and the decision will be based on the needs of the task you're attempting to accomplish. Ultimately, your app will have a combination of both tests, but how do you choose right now?

Let's review each of these test types, their benefits, and what to consider when testing your Next.js application.

End-to-End Testing

E2E Testing is a technique for testing your app from the web browser to the back end, as well as interactions with third-party APIs and services. These tests ensure that your entire software works as a unit.

Cypress does end-to-end testing work in the same way users interact with your app using a real browser to visit URLs, view content, click on links and buttons, and so on. This testing method helps guarantee that your tests and the user's experience are consistent. Tests are written in code and use an API to replicate what a real user would do.

Benefits of End-to-End tests

  • Ensures your app functions as a unit.
  • Testing corresponds to the user experience.
  • Developers or QA Teams can write it.
  • It can also be used for integration testing.

Considerations for End-to-End tests

  • More complex and time-consuming to set up, execute, and maintain.
  • Provide testing infrastructure in CI.
  • Testing specific situations necessitates additional setup.

Writing your first E2E test

Cypress supports most browsers, including Chrome, Firefox, and Microsoft's new Edge browser. Cypress's execution speed and debugging capabilities are enhanced because the tests run within the browser. While the tests are performed in the browser, Cypress uses a node server as a proxy.

The browser and the Cypress Node process continually communicate to do duties on each other's behalf, such as simulating network requests from tests. Cypress test suites may be readily integrated into any CI/CD pipeline to perform end-to-end tests during their nightly cycles, per pull request, or other triggered events.

End-to-End Testing Launchpad

Cypress has provided some default tests to run your first E2E test in the Next.js app, as shown below.

Cypress default tests

Instead of using the provided tests, head to the file directory: cypress/e2e and create a new file called sample.cy.js. After creating the file, copy and paste the code snippet below.

#cypress/e2e/1-getting-started/sample.cy.js

describe('My First Test', () => {
  it('clicks the link "type"', () => {
    cy.visit('https://example.cypress.io')
    cy.contains('type').click()
  })
})
Enter fullscreen mode Exit fullscreen mode

The test visits a webpage and clicks on a link located on the webpage.

First test

Component Testing

Component tests differ from end-to-end testing in that, rather than visiting a URL to load up an entire app, a component can be "mounted" and evaluated independently. This allows you to focus solely on testing the component's functionality and not on any intricacies associated with testing a component as part of a bigger application.

Modern web frameworks provide ways to write applications by splitting them into smaller logical units known as components. Components might be as simple as a button to something as complicated as a registration form. Because of their nature, components are easily testable, which is where Cypress Component Testing comes in.

Nevertheless, even if all of your component tests pass, this does not imply that your application functions properly. Component tests offer nothing to confirm that all of your app's layers work together correctly. As a result, a well-tested program includes both end-to-end and component tests, with each set of tests specializing in what it does best.

Benefits of Component Tests

  • It is simpler to test components in isolation.
  • Quick and dependable.
  • It is simple to create customized scenarios in tests.
  • They don't rely on an external system to function.

Considerations for Component Tests

  • Do not guarantee overall app quality.
  • Do not use external APIs/Services.
  • Often written by developers working on the component.

Writing your first Component test

Navigate to the file directory, pages and create two files - Counter.js and Counter.cy.js. These files are the app’s component and testing files. Head to Counter.js and paste the code snippet below.

import { useState } from "react";
export default function Counter({ initial = 0, onChange = () => {} }) {
  const [count, setCount] = useState(initial);
  const handleIncrement = () => {
    const newCount = count + 1;
    setCount(newCount);
    onChange(newCount);
  };
  const handleDecrement = () => {
    const newCount = count - 1;
    setCount(newCount);
    onChange(newCount);
  };
  return (
    <div>
      <h1>Counter</h1>
      <button data-cy="decrement" className="btn-primary" onClick={handleDecrement}>
        -
      </button>
      <span data-cy="counter" className="counter">{count}</span>
      <button data-cy="increment" className="btn-primary" onClick={handleIncrement}>
        +
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This will create a simple counter to increase or decrease a number. The application will look like this after applying the code snippet above.

Counter

Now, let’s create a test script for the Counter.js file. Head to the Counter.cy.js file and write a test to mount the Counter.js component, then click on the increment button. After clicking the increment button, Cypress should check if the counter has a value of 1. Cypress should also do the same in a decrement scenario.

import React from 'react'
import Counter from './Counter'

it('when the increment button is pressed, the counter is incremented', () => {
  cy.mount(<Counter />)
  cy.get('[data-cy=increment]').click()
  cy.get('[data-cy=counter]').should('have.text', '1')
})
it('when the decrement button is pressed, the counter is decremented', () => {
  cy.mount(<Counter />)
  cy.get('[data-cy=decrement]').click()
  cy.get('[data-cy=counter]').should('have.text', '-1')
})
Enter fullscreen mode Exit fullscreen mode

Head to the Component Test Launchpad and access the component testing page to run the test file you just created.

Component Testing Launchpad
Component testing page

The test visits the component file and performs the test from its testing file.

Counter Test

Testing Type Comparison

While both end-to-end testing and component testing are valuable testing methodologies, they differ in terms of their scope, granularity, and complexity. These are comparisons of the two testing types:

E2E Component
What is tested in the app All app layers Individual component
Characteristics of the test type Comprehensive, slower, more susceptible to flake Specialized, quick, reliable
Test type use Verifying app works as a cohesive whole Testing the functionality of an individual component
Written by Developers, QA Team, SDETs Developers, Designers
CI Infrastructure Often requires complex setup None needed
Initialization Command cy.visit(url) cy.mount(<MyComponent />)

Testing API routes

When working with larger applications, you may encounter the need to test routes and endpoints. This is also possible with Cypress. Navigate to the pages folder in your application and create a new file called books.js, which will store the data and functions you need for your API:

// pages/api/books.js

export default function books(req, res) {
  res.statusCode = 200;
  res.setHeader("Content-Type", "application/json");
  return res.json([
    {
      id: 1,
      book: "The Firm",
      author: "John Grisham",
    },
    {
      id: 2,
      book: "Cracking the PM interview",
      author: "Jackie Bavaro",
    },
    {
      id: 3,
      book: "Fools Die",
      author: "Mario Puzo",
    },
  ]);
}
Enter fullscreen mode Exit fullscreen mode

Next, let’s create a script for testing. Navigate to the e2e/1-getting-started folder in cypress, where you’ll create the tests for the API routes:

//cypress/e2e/1-getting-started/books.cy.js

describe("Book test", () => {
  it("Confirms the number of books in your local library", () => {
    cy.visit("http://localhost:3000");
    cy.request("GET", "api/books").as("books");
    cy.get("@books").should((response) => {
      expect(response.status).to.eq(200);
      expect(response).to.have.property("headers");
      expect(response).to.have.property("duration");
      expect(response.body).to.have.length(3);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

The test expects the following from the books endpoint’s response:

  • Response status equals 200.
  • The API response to include a header.
  • The API response body should contain only three objects.
  • The test should include response time.

Launch the E2E Test server and run the tests like before.

You should have this as your result after performing the test.

Test Isolation

Test Isolation refers to the ability to execute tests independently of one another without being affected by the state of other tests or the application itself. Developers can ensure that other tests or external influences do not influence the results of each test by executing each test in a clean environment. This facilitates the identification and correction of bugs and the efficient collaboration of team members.

Test Isolation in End-to-End Testing
End-to-end testing in Cypress supports enabling or disabling test isolation to specify whether a suite of tests should run in a clean browser context or not. The test isolation is a global configuration that can be overridden at the describe level for End-to-End testing with the testIsolation option.

#cypress.config.js

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    testIsolation: true
  },
})
Enter fullscreen mode Exit fullscreen mode

When test isolation is enabled, Cypress resets the browser context before each test by:

Because the test starts in a new browser context, you must return to your application and perform the set of interactions required to build the DOM and browser state for each test.

Also, when establishing a browser session, the cy.session(0) command will inherit this configuration and erase the page and current browser context. This is done so that tests can pass when run independently or in a randomized order.

When test isolation is disabled, Cypress doesn't change the browser context before the test begins. The page does not distinguish between tests and cookies; therefore, local and session storage will be accessible across all tests in that suite. Also, the cy.session() command clears only the current browser context while establishing a browser session.

Test Isolation in Component Testing
Cypress doesn’t support global configuration for test isolation in component testing.
When running component tests, Cypress always resets the browser context before each test by:

It is crucial to remember that while disabling test isolation may increase overall end-to-end test performance, it may also allow the state to "leak" between tests. This can make recent tests reliant on the results of previous tests, potentially leading to misleading test failures. While using this mode, it is important to be particularly attentive to how tests are written and ensure that tests continue running independently.

Conclusion

Using Cypress to test Next.js apps can assist developers in creating high-quality, reliable web applications that fulfill user experience and standards and perform well in production.

Resources

The following resources can help you get started with Cypress:

Top comments (0)