//
Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Teste Server Components, Server Actions, Route Handlers e Client Components em um projeto Next.js 15+ App Router usando Vitest e React Testing Library.
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.ts"],
include: ["**/*.test.{ts,tsx}"],
alias: {
"@": path.resolve(__dirname, "."),
},
},
});// vitest.setup.ts
import "@testing-library/jest-dom/vitest";// package.json (scripts)
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}// components/counter.tsx
"use client";
import { useState } from "react";
export function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p data-testid="count">Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</div>
);
}// components/counter.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Counter } from "./counter";
describe("Counter", () => {
it("renders initial count", () => {
render(<Counter initialCount={5} />);
expect(screen.getByTestId("count")).toHaveTextContent("Count: 5");
});
it("increments on click", () => {
render(<Counter />);
fireEvent.click(screen.getByText("Increment"));
expect(screen.getByTestId("count")).toHaveTextContent("Count: 1");
});
});// app/posts/post-list.tsx
import { db } from "@/lib/db";
export async function PostList() {
const posts = await db.post.findMany();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}// app/posts/post-list.test.tsx
import { describe, it, expect, vi } from "vitest";
vi.mock("@/lib/db", () => ({
db: {
post: {
findMany: vi.fn().mockResolvedValue([
{ id: "1", title: "First Post" },
{ id: "2", title: "Second Post" },
]),
},
},
}));
describe("PostList", () => {
it("renders posts from database", async () => {
// Import after mocking
const { PostList } = await import("./post-list");
// Await the Server Component (it's an async function)
const jsx = await PostList();
// Use render to test the returned JSX
const { render } = await import("@testing-library/react");
const { getAllByRole } = render(jsx);
const items = getAllByRole("listitem");
expect(items).toHaveLength(2);
expect(items[0]).toHaveTextContent("First Post");
});
});// app/actions.ts
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
if (!title || title.length < 3) {
return { error: "Title must be at least 3 characters" };
}
await db.post.create({ data: { title } });
revalidatePath("/posts");
return { success: true };
}// app/actions.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("@/lib/db", () => ({
db: {
post: {
create: vi.fn().mockResolvedValue({ id: "1", title: "Test" }),
},
},
}));
vi.mock("next/cache", () => ({
revalidatePath: vi.fn(),
}));
describe("createPost", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns error for short title", async () => {
const { createPost } = await import("./actions");
const formData = new FormData();
formData.set("title", "ab");
const result = await createPost(formData);
expect(result).toEqual({ error: "Title must be at least 3 characters" });
});
it("creates post and revalidates", async () => {
const { createPost } = await import("./actions");
const { db } = await import("@/lib/db");
const { revalidatePath } = await import("next/cache");
const formData = new FormData();
formData.set("title", "Valid Title");
const result = await createPost(formData);
expect(result).toEqual({ success: true });
expect(db.post.create).toHaveBeenCalledWith({
data: { title: "Valid Title" },
});
expect(revalidatePath).toHaveBeenCalledWith("/posts");
});
});// app/api/posts/route.test.ts
import { describe, it, expect, vi } from "vitest";
import { NextRequest } from "next/server";
vi.mock("@/lib/db", () => ({
db: {
post: {
findMany: vi.fn().mockResolvedValue([{ id: "1", title: "Test" }]),
},
},
}));
describe("GET /api/posts", () => {
it("returns posts as JSON", async () => {
const { GET } = await import("./route");
const request = new NextRequest("http://localhost:3000/api/posts");
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.posts).toHaveLength(1);
});
});"use server". Remova a diretiva fazendo o mock da fronteira do módulo e teste-as como funções async simples.vi.mock() deve ser chamado antes de importar o módulo a ser testado. Use import() dinâmico após vi.mock() para garantir que os mocks sejam aplicados.revalidatePath e revalidateTag de next/cache devem ter seus mocks definidos para evitar erros de tempo de execução e para verificar se foram chamados corretamente.NextRequest.Testes de Integração com Playwright:
// e2e/posts.spec.ts
import { test, expect } from "@playwright/test";
test("creates a post", async ({ page }) => {
await page.goto("/posts/new");
await page.fill('[name="title"]', "My New Post");
await page.click('button[type="submit"]');
await expect(page.locator("text=My New Post")).toBeVisible();
});Testando com MSW (Mock Service Worker):
// mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/posts", () => {
return HttpResponse.json([
{ id: "1", title: "Mocked Post" },
]);
}),
];vi.fn<Parameters<typeof fn>, ReturnType<typeof fn>>() para mocks totalmente tipados.FormData está disponível globalmente no Vitest com o ambiente jsdom. Nenhum polyfill é necessário.NextRequest de next/server nos testes. Ele funciona fora do runtime do Next.js para construir objetos de requisição."use server" é ignorada em ambientes de teste. Server Actions são apenas funções async quando importadas diretamente. Isso é aceitável para testes unitários, mas significa que você não está testando a fronteira de serialização.cookies() e headers() devem ter seus mocks definidos. Eles lançam erros fora do contexto de requisição do Next.js. Faça o mock deles com vi.mock("next/headers").await a função do componente primeiro e depois passe o resultado para render().redirect() lança um erro em testes. Faça o mock de next/navigation ou envolva as chamadas em try/catch e afirme o erro lançado.next/image ou next/link automaticamente. Crie mocks manuais ou use vi.mock() para contorná-los.| Abordagem | Prós | Contras |
|---|---|---|
| Vitest + RTL | Rápido, moderno, nativo ESM | Mocking manual do Next.js necessário |
| Jest + RTL | Grande ecossistema, amplamente documentado | Mais lento, orientado a CJS, precisa de transformação SWC |
| Playwright | Navegador completo, integração real | Mais lento, requer servidor em execução |
| Cypress | Depuração visual, testes de componentes | Mais pesado, não ideal para Server Components |
| next/experimental/testing | Utilitários oficiais de teste do Next.js | Experimental, superfície de API limitada |
await (ex: const jsx = await PostList()).render().vi.mock() para o topo do arquivo, mas import() dinâmico não é elevado.vi.mock(), o módulo real será usado em vez do mock.await import("./module") após vi.mock() para garantir que os mocks sejam aplicados.vi.mock("next/headers", () => ({
cookies: vi.fn().mockResolvedValue({
get: vi.fn().mockReturnValue({ value: "mock-token" }),
}),
headers: vi.fn().mockResolvedValue(new Headers()),
}));"use server" é ignorada quando você importa Server Actions diretamente em testes.GET, POST) do arquivo route.ts.new NextRequest("http://localhost:3000/api/endpoint") e passe-o para a função.Response retornado.redirect() lança internamente um erro especial NEXT_REDIRECT.next/navigation com vi.mock() ou envolva a chamada em try/catch e afirme o erro lançado.next/cache dependem do contexto do runtime do Next.js.import { vi } from "vitest";
import type { db } from "@/lib/db";
const mockCreate = vi.fn<
Parameters<typeof db.post.create>,
ReturnType<typeof db.post.create>
>();next/image têm internos complexos que falham em um ambiente jsdom simples.vi.mock("next/image", ...) para contorná-los em testes.const formData = new FormData();
formData.set("title", "My Post");
const result = await createPost(formData);FormData está disponível globalmente no Vitest com o ambiente jsdom.Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥