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.