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:
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:
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:
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:
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:
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!
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.