//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Prueba React Server Components, Server Actions, Route Handlers y Client Components en un proyecto Next.js 15+ App Router usando Vitest y 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">Conteo: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Incrementar</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("renderiza el conteo inicial", () => {
render(<Counter initialCount={5} />);
expect(screen.getByTestId("count")).toHaveTextContent("Conteo: 5");
});
it("incrementa al hacer clic", () => {
render(<Counter />);
fireEvent.click(screen.getByText("Incrementar"));
expect(screen.getByTestId("count")).toHaveTextContent("Conteo: 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("renderiza posts de la base de datos", async () => {
// Importar después de mockear
const { PostList } = await import("./post-list");
// Espera el Server Component (es una función asincronía)
const jsx = await PostList();
// Usa render para probar el JSX devuelto
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: "El título debe tener al menos 3 caracteres" };
}
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("devuelve error para título corto", async () => {
const { createPost } = await import("./actions");
const formData = new FormData();
formData.set("title", "ab");
const result = await createPost(formData);
expect(result).toEqual({ error: "El título debe tener al menos 3 caracteres" });
});
it("crea post y revalida", 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("devuelve posts como 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". Elimina la directiva mockeando el límite del módulo y prueba los como funciones asincronías simples.vi.mock() debe ser llamado antes de importar el módulo bajo prueba. Usa import() dinámico después de vi.mock() para asegurar que los mocks se aplican.revalidatePath y revalidateTag de next/cache deben ser mockeados para prevenir errores en tiempo de ejecución y para verificar que sean llamados correctamente.NextRequest.Pruebas de Integración con Playwright:
// e2e/posts.spec.ts
import { test, expect } from "@playwright/test";
test("crea un 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();
});Pruebas con 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 completamente tipados.FormData está disponible globalmente en Vitest con el entorno jsdom. No se necesita polyfill.NextRequest de next/server en las pruebas. Funciona fuera del tiempo de ejecución de Next.js para construir objetos de solicitud."use server" se ignora en entornos de prueba. Server Actions son solo funciones asincronías cuando se importan directamente. Está bien para pruebas unitarias pero significa que no estás probando el límite de serialización.cookies() y headers() deben ser mockeados. Lanzan fuera del contexto de solicitud de Next.js. Mockea los con vi.mock("next/headers").await la función del componente primero, luego pasa el resultado a render().redirect() lanza en pruebas. Mockea next/navigation o envuelve las llamadas en try/catch y aserta el error lanzado.next/image o next/link automáticamente. Crea mocks manuales o usa vi.mock() para stubear los.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Vitest + RTL | Rápido, moderno, nativo de ESM | Mock manual de Next.js requerido |
| Jest + RTL | Gran ecosistema, ampliamente documentado | Más lento, orientado a CJS, necesita transformación SWC |
| Playwright | Navegador completo, integración verdadera | Más lento, requiere servidor en ejecución |
| Cypress | Depuración visual, pruebas de componentes | Más pesado, no ideal para Server Components |
| next/experimental/testing | Utilidades de prueba oficiales de Next.js | Experimental, superficie de API limitada |
await (por ejemplo, const jsx = await PostList()).render().vi.mock() a la parte superior del archivo, pero import() dinámico no se eleva.vi.mock(), se usa el módulo real en lugar del mock.await import("./module") después de vi.mock() para asegurar que los mocks se aplican.vi.mock("next/headers", () => ({
cookies: vi.fn().mockResolvedValue({
get: vi.fn().mockReturnValue({ value: "mock-token" }),
}),
headers: vi.fn().mockResolvedValue(new Headers()),
}));"use server" se ignora cuando importas Server Actions directamente en pruebas.GET, POST) del archivo route.ts.new NextRequest("http://localhost:3000/api/endpoint") y pásalo a la función.redirect() lanza un error especial NEXT_REDIRECT internamente.next/navigation con vi.mock() o envuelve la llamada en try/catch y aserta el error lanzado.next/cache dependen del contexto de tiempo de ejecución de 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 tienen internos complejos que fallan en un entorno jsdom simple.vi.mock("next/image", ...) para stubear los en pruebas.const formData = new FormData();
formData.set("title", "My Post");
const result = await createPost(formData);FormData está disponible globalmente en Vitest con el entorno jsdom.Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥