ProNextJS
    Loading
    lesson

    Testing with Vitest

    Jack HerringtonJack Herrington

    In this lesson, we'll explore how to set up and use the vitest framework to write unit tests for your Next.js application.

    Setting up the Next.js Application

    To get started, we'll create a new Next.js application called "testing-with-vitest" and select our standard configuration options with TypeScript, ESLint, Tailwind CSS, and the App Router structure:

    pnpm dlx create-next-app@latest testing-with-vitest --use pnpm
    

    Next, we'll add the necessary dev dependencies to support the testing framework. These dependencies fall into two main categories: Testing Framework Dependencies and JSDOM & Testing Library Dependencies.

    For the testing framework, we'll install the core vitest library and the React plugin @vitejs/plugin-react as well as the @vitest/ui package. For JSDOM and Testing Library support, we'll install jsdom and the react and user-event plugins for testing-library:

    pnpm add vitest @vitejs/plugin-react @vitest/ui jsdom @testing-library/react @testing-library/user-event -D
    

    These dependencies allow us to test React components efficiently by rendering them in memory using JSDOM, rather than spinning up a browser for each test.

    However, the core concepts we discuss here can be applied to testing any kind of code.

    Configuring vitest

    To configure vitest, create a new file called vitest-config.ts in the project root with the following code:

    import { defineConfig } from "vitest/config";
    import react from "@vitejs/plugin-react";
    
    export default defineConfig({
      plugins: [react()],
      test: {
        environment: "jsdom",
      }
    });
    

    In this file, we specify the React plugin and set the testing environment to JSDOM, making it easier to test React components.

    Next, in the package.json file add the following scripts to run the tests:

    {
      "scripts": {
        ...
        "test": "vitest run",
        "test:ui": "vitest --ui",
        "test:watch": "vitest --watch"
      }
      ...
    

    Writing a Simple Test

    Let's simplify the Home component to make it easier to test. Replace the existing code in app/page.tsx with the following:

    // inside app/page.tsx
    
    export default function Home() {
      return (
        <main>
          <h1>Counter Test</h1>
        </main>
      );
    }
    

    Next, we'll create a new file called app/page.test.tsx next to the page.tsx file:

    import { expect, test } from "vitest";
    import { render, screen } from "@testing-library/react";
    import HomePage from "./page";
    
    test("Basic page test", () => {
      render(<HomePage />);
      expect(screen.getByText("Counter Test")).toBeDefined();
    });
    

    In this test file, we import the necessary modules from vitest and @testing-library/react, as well as the Home component. We write a simple test using the test function from vitest, render the Home component using the render function from the testing library, and assert that the rendered component contains the text "Counter Test".

    Running the Tests

    In the terminal, run pnpm test and see the test pass successfully:

    the test passes

    You can also run the tests in watch mode using pnpm test:watch or launch the test UI with pnpm test:ui.

    Here's how the test UI looks:

    the vitest ui

    If you change the text in the Home component and save the file, the test will re-run automatically and report a failure since the expected text has changed:

    the ui showing the failing test

    Testing a Client Component

    Now that we have one test passing, let's create a new client component called Counter and test it.

    Create a new file app/Counter.tsx with the following code:

    // app/Counter.tsx
    "use client";
    
    import { useState } from "react";
    
    export default function Counter() {
      const [count, setCount] = useState(1);
    
      return (
        <div>
          <p data-testid="count">Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increment</button>
          <button onClick={() => setCount(count - 1)}>Decrement</button>
        </div>
      );
    }
    

    Note that the p tag has a data-testid of count that will help us target it in our test.

    Update the app/page.tsx file to include the Counter component:

    // inside app/page.tsx
    import Counter from './counter';
    
    export default function Home() {
      return (
        <main>
          <h1>Counter Test</h1>
          <Counter />
        </main>
      );
    }
    

    Starting the app with pnpm dev should show the unstyled counter component on the home page:

    unstyled counter

    To test the Counter component, create a new file app/Counter.test.tsx:

    import { expect, test } from "vitest";
    import { render, screen } from "@testing-library/react";
    import userEvent from "@testing-library/user-event";
    import Counter from "./Counter";
    
    test("tests a counter", async () => {
      render(<Counter />);
      await userEvent.click(screen.getByText("Increment"));
      expect(screen.getByTestId("count")).toHaveTextContent("Count: 2");
    });
    

    In this test, we render the Counter component, click the increment button using userEvent, and assert that the count display has the expected text content of "Count: 2".

    When trying to run the tests at this point, there will be an error that toHaveTextContent is not a valid matcher:

    error with toHaveTextContent

    To fix the "toHaveTextContent" error, we need to import the @testing-library/jest-dom/vitest package which we will do in a setup file called vitest-setup.ts:

    // vitest-setup.ts
    import '@testing-library/jest-dom/vitest';
    

    Then we need to update the vitest-config.ts file to include the setup file:

    // vitest-config.ts
    import { defineConfig } from 'vitest';
    import react from '@vitest/react';
    
    export default defineConfig({
      plugins: [react()],
      test: {
        environment: 'jsdom',
        setupFiles: ['./vitest-setup.ts'],
      },
    });
    

    Now, running the tests should show that both tests pass!

    tests are passing

    Recap

    In this lesson, we covered how to set up and use vitest to write unit tests for your Next.js application, focused on testing both React Server Components and client components.

    When a more "official" way to test asynchronous React Server Components becomes available, we'll update this lesson with the necessary information.

    Transcript