Testando Componentes e Ações de Servidor
Teste Componentes de Servidor assíncronos, Ações de Servidor e Route Handlers tratando-os como funções assíncronas simples.
Busque em todas as páginas da documentação
Teste Componentes de Servidor assíncronos, Ações de Servidor e Route Handlers tratando-os como funções assíncronas simples.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida -- pronto para copiar e colar.
// Teste um Componente de Servidor assíncrono como uma função
import { render, screen } from "@testing-library/react";
// Componentes de Servidor são funções assíncronas -- chame e renderize sua saída
const Component = await ServerComponent({ params: { id: "1" } });
render(Component);
expect(screen.getByText("Alice")).toBeInTheDocument();
// Teste uma Ação de Servidor como uma função unitária
const formData = new FormData();
formData.set("email", "alice@example.com");
const result = await createUser(formData);
expect(result.success).toBe(true);
// Teste um Route Handler como uma função
const request = new Request("http://localhost/api/users", { method: "GET" });
const response = await GET(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data).toHaveLength(3);Quando usar isso: Ao testar Componentes de Servidor, Ações de Servidor ou Route Handlers do App Router do Next.js em isolamento.
// src/lib/db.ts
export interface User {
id: string;
name: string;
email: string;
}
export async function getUserById(id: string): Promise<User | null> {
// Em produção, isso consulta um banco de dados
throw new Error("Não implementado -- use mock em testes");
}
export async function createUser(data: Omit<User, "id">): Promise<User> {
throw new Error("Não implementado -- use mock em testes");
}
export async function getAllUsers(): Promise<User[]> {
throw new Error("Não implementado -- use mock em testes");
}// src/app/users/[id]/page.tsx
import { getUserById } from "@/lib/db";
import { notFound } from "next/navigation";
interface Props {
params: Promise<{ id: string }>;
}
export default async function UserPage({ params }: Props) {
const { id } = await params;
const user = await getUserById(id);
if (!user) {
notFound();
}
return (
<article>
<h1>{user.name}</h1>
<p>{user.email}</p>
</article>
);
}// src/app/users/[id]/page.test.tsx
import { render, screen } from "@testing-library/react";
import { vi, describe, it, expect } from "vitest";
import UserPage from "./page";
// Mock do módulo do banco de dados
vi.mock("@/lib/db", () => ({
getUserById: vi.fn(),
}));
// Mock do next/navigation
const mockNotFound = vi.fn();
vi.mock("next/navigation", () => ({
notFound: () => {
mockNotFound();
throw new Error("NEXT_NOT_FOUND");
},
}));
import { getUserById } from "@/lib/db";
describe("UserPage", () => {
it("renderiza dados do usuário", async () => {
vi.mocked(getUserById).mockResolvedValue({
id: "1",
name: "Alice Johnson",
email: "alice@example.com",
});
// Componentes de Servidor Assíncronos retornam JSX -- aguarde e depois renderize
const jsx = await UserPage({ params: Promise.resolve({ id: "1" }) });
render(jsx);
expect(screen.getByRole("heading")).toHaveTextContent("Alice Johnson");
expect(screen.getByText("alice@example.com")).toBeInTheDocument();
expect(getUserById).toHaveBeenCalledWith("1");
});
it("chama notFound quando o usuário não existe", async () => {
vi.mocked(getUserById).mockResolvedValue(null);
await expect(
UserPage({ params: Promise.resolve({ id: "999" }) })
).rejects.toThrow("NEXT_NOT_FOUND");
expect(mockNotFound).toHaveBeenCalled();
});
});// src/app/actions/user-actions.ts
"use server";
import { z } from "zod";
import { createUser as dbCreateUser } from "@/lib/db";
import { revalidatePath } from "next/cache";
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
interface ActionState {
success: boolean;
message: string;
errors: Record<string, string>;
}
export async function createUserAction(
_prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const rawData = {
name: formData.get("name"),
email: formData.get("email"),
};
const parsed = createUserSchema.safeParse(rawData);
if (!parsed.success) {
const errors: Record<string, string> = {};
for (const issue of parsed.error.issues) {
errors[issue.path[0] as string] = issue.message;
}
return { success: false, message: "Falha na validação", errors };
}
await dbCreateUser(parsed.data);
revalidatePath("/users");
return { success: true, message: "Usuário criado", errors: {} };
}// src/app/actions/user-actions.test.ts
import { vi, describe, it, expect, beforeEach } from "vitest";
import { createUserAction } from "./user-actions";
// Mock de dependências
vi.mock("@/lib/db", () => ({
createUser: vi.fn().mockResolvedValue({ id: "new-1", name: "Alice", email: "alice@test.com" }),
}));
vi.mock("next/cache", () => ({
revalidatePath: vi.fn(),
}));
import { createUser } from "@/lib/db";
import { revalidatePath } from "next/cache";
describe("createUserAction", () => {
const initialState = { success: false, message: "", errors: {} };
beforeEach(() => {
vi.clearAllMocks();
});
it("cria um usuário com dados válidos", async () => {
const formData = new FormData();
formData.set("name", "Alice Johnson");
formData.set("email", "alice@example.com");
const result = await createUserAction(initialState, formData);
expect(result).toEqual({
success: true,
message: "Usuário criado",
errors: {},
});
expect(createUser).toHaveBeenCalledWith({
name: "Alice Johnson",
email: "alice@example.com",
});
expect(revalidatePath).toHaveBeenCalledWith("/users");
});
it("retorna erros de validação para dados inválidos", async () => {
const formData = new FormData();
formData.set("name", "A"); // muito curto
formData.set("email", "not-an-email");
const result = await createUserAction(initialState, formData);
expect(result.success).toBe(false);
expect(result.errors.name).toBeDefined();
expect(result.errors.email).toBeDefined();
expect(createUser).not.toHaveBeenCalled();
});
it("retorna erro de validação para campos ausentes", async () => {
const formData = new FormData();
const result = await createUserAction(initialState, formData);
expect(result.success).toBe(false);
expect(Object.keys(result.errors).length).toBeGreaterThan(0);
});
});// src/app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAllUsers, createUser } from "@/lib/db";
export async function GET() {
const users = await getAllUsers();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.name || !body.email) {
return NextResponse.json(
{ error: "Nome e email são obrigatórios" },
{ status: 400 }
);
}
const user = await createUser(body);
return NextResponse.json(user, { status: 201 });
}// src/app/api/users/route.test.ts
import { vi, describe, it, expect, beforeEach } from "vitest";
import { GET, POST } from "./route";
import { NextRequest } from "next/server";
vi.mock("@/lib/db", () => ({
getAllUsers: vi.fn().mockResolvedValue([
{ id: "1", name: "Alice", email: "alice@test.com" },
{ id: "2", name: "Bob", email: "bob@test.com" },
]),
createUser: vi.fn().mockResolvedValue({
id: "3",
name: "Charlie",
email: "charlie@test.com",
}),
}));
describe("GET /api/users", () => {
it("retorna todos os usuários", async () => {
const response = await GET();
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveLength(2);
expect(data[0].name).toBe("Alice");
});
});
describe("POST /api/users", () => {
it("cria um usuário com dados válidos", async () => {
const request = new NextRequest("http://localhost/api/users", {
method: "POST",
body: JSON.stringify({ name: "Charlie", email: "charlie@test.com" }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(201);
expect(data.name).toBe("Charlie");
});
it("retorna 400 para campos ausentes", async () => {
const request = new NextRequest("http://localhost/api/users", {
method: "POST",
body: JSON.stringify({ name: "Charlie" }),
});
const response = await POST(request);
expect(response.status).toBe(400);
});
});O que isso demonstra:
notFound() de next/navigationrevalidatePathrender()FormData -- você as chama diretamente com FormData de testeGET, POST, etc.) que aceitam Request e retornam Response -- objetos padrão da Web APInotFound() do Next.js lança um erro especial internamente -- mocke-o para lançar um erro que você possa capturar em testesrevalidatePath e revalidateTag são APIs de cache do Next.js que não têm efeito em testes -- mocke-as para verificar se foram chamadasTestando um Componente de Servidor com search params:
// src/app/users/page.tsx
interface Props {
searchParams: Promise<{ q?: string; page?: string }>;
}
export default async function UsersPage({ searchParams }: Props) {
const { q, page } = await searchParams;
// ...
}
// Teste
const jsx = await UsersPage({
searchParams: Promise.resolve({ q: "alice", page: "2" }),
});
render(jsx);Testando Ações de Servidor com transações de banco de dados:
vi.mock("@/lib/db", () => ({
db: {
transaction: vi.fn((fn) => fn({
insert: vi.fn().mockResolvedValue([{ id: "1" }]),
update: vi.fn().mockResolvedValue([]),
})),
},
}));// Tipando props de Componente de Servidor para testes
import type { Metadata } from "next";
// Componentes de Servidor também podem exportar generateMetadata
export async function generateMetadata({ params }: Props): Promise<Metadata> {
// Teste isso como uma função assíncrona regular
}
// Construção de NextRequest para testes de Route Handler
const request = new NextRequest("http://localhost/api/users?page=2", {
method: "GET",
headers: { "Content-Type": "application/json" },
});Não é possível renderizar() um componente assíncrono diretamente -- O render do React não suporta componentes assíncronos. Correção: Aguarde a chamada da função do Componente de Servidor e, em seguida, passe o JSX resultante para render().
Diretiva "use server" em arquivos de teste -- Vitest trata arquivos como módulos normais. A diretiva "use server" é ignorada. Correção: Isso é bom para testes -- Ações de Servidor são apenas funções assíncronas no contexto de teste.
Mockando next/headers -- Funções como cookies() e headers() só funcionam em um contexto de servidor. Correção: Mocke o módulo inteiro:
vi.mock("next/headers", () => (\{
cookies: () => (\{
get: vi.fn().mockReturnValue(\{ value: "session-token" \}),
set: vi.fn(),
\}),
headers: () => new Headers(\{ "x-user-id": "123" \}),
\}));Componentes de Servidor não podem ser testados com interatividade do lado do cliente -- Você só pode testar sua saída renderizada, não cliques ou mudanças de estado. Correção: Teste a saída HTML/JSX renderizada. Teste componentes de cliente interativos separadamente.
FormData em testes -- FormData.get() retorna FormDataEntryValue (string ou File), que pode precisar de casting em schemas tipados. Correção: Zod lida com a coerção. Em testes, formData.set("key", "value") sempre define strings.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Playwright E2E | Você precisa testar Componentes de Servidor com renderização completa do Next.js | Você quer testes rápidos em nível de unidade |
| Storybook RSC | Você quer desenvolver e visualizar Componentes de Servidor isoladamente | Você precisa de testes baseados em afirmações |
| Testes de Integração | Você quer testar o ciclo completo de requisição/resposta | Testes unitários rápidos de funções individuais são suficientes |
next/test (experimental) | Next.js lança utilitários de teste oficiais para RSC | A API ainda não é estável |
Chame-o como uma função, aguarde o resultado e, em seguida, renderize:
const jsx = await UserPage({ params: Promise.resolve({ id: "1" }) });
render(jsx);
expect(screen.getByText("Alice")).toBeInTheDocument();O render do React não suporta componentes assíncronos. Você deve aguardar a chamada da função do Componente de Servidor primeiro e, em seguida, passar o JSX resultante para render().
Trate-a como uma função assíncrona simples. Construa FormData, chame a ação e afirme o resultado:
const formData = new FormData();
formData.set("email", "alice@example.com");
const result = await createUserAction(initialState, formData);
expect(result.success).toBe(true);const mockNotFound = vi.fn();
vi.mock("next/navigation", () => ({
notFound: () => {
mockNotFound();
throw new Error("NEXT_NOT_FOUND");
},
}));Em seguida, afirme com expect(...).rejects.toThrow("NEXT_NOT_FOUND").
Não. Vitest trata arquivos como módulos normais, então a diretiva "use server" é ignorada. Ações de Servidor são apenas funções assíncronas no contexto de teste, o que é bom para testes.
vi.mock("next/headers", () => ({
cookies: () => ({
get: vi.fn().mockReturnValue({ value: "session-token" }),
set: vi.fn(),
}),
headers: () => new Headers({ "x-user-id": "123" }),
}));Construa um objeto Request ou NextRequest, chame a função exportada e afirme a resposta:
const request = new NextRequest("http://localhost/api/users", {
method: "POST",
body: JSON.stringify({ name: "Alice", email: "a@b.com" }),
});
const response = await POST(request);
expect(response.status).toBe(201);Mocke next/cache e afirme:
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
// ... chame sua ação de servidor
expect(revalidatePath).toHaveBeenCalledWith("/users");Não. Componentes de Servidor produzem apenas saída HTML/JSX. Você pode testar a saída renderizada, mas não interações do lado do cliente. Teste componentes de cliente interativos separadamente.
const jsx = await UsersPage({
searchParams: Promise.resolve({ q: "alice", page: "2" }),
});
render(jsx);const request = new NextRequest("http://localhost/api/users?page=2", {
method: "GET",
headers: { "Content-Type": "application/json" },
});NextRequest estende o Request padrão e é totalmente tipado a partir de next/server.
vi.mock("@/lib/db", () => ({
db: {
transaction: vi.fn((fn) => fn({
insert: vi.fn().mockResolvedValue([{ id: "1" }]),
update: vi.fn().mockResolvedValue([]),
})),
},
}));Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥