Next.js: Testes Unitários e de Integração
Última atualização: 2026-08-26
Testes não são opcionais — são o "airbag" das aplicações em produção: você não precisa deles no dia a dia, mas podem salvar sua vida quando realmente importa.
1. O Que Você Vai Aprender
- Configurar um ambiente de testes no Next.js 16 usando Vitest (vitest.config.ts + camada de compatibilidade React 17M)
- Renderizar e fazer asserções em componentes Server e Client usando @testing-library/react
- Escrever testes de integração para Server Actions (Mockando Prisma + Simulando revalidatePath)
- Usar matchers personalizados do jest-dom (toBeInTheDocument / toHaveTextContent)
- Usar MSW para interceptar requisições de API externas e isolar dependências de rede
2. A História Real de uma Engenheira Full-Stack
(1) Problema: Na véspera do lançamento, um único espaço fez a página de pagamento quebrar
Alice trabalha como engenheira full-stack em uma plataforma de e-commerce que atende o mercado do Oriente Médio. A plataforma processa mais de 50.000 pedidos por dia, e a equipe mantém um ciclo de lançamento de duas vezes por semana.
Durante o lançamento da última sexta-feira, uma alteração aparentemente inofensiva — adicionar um espaço extra na função de formatação de preço no componente OrderSummary — fez com que os valores de pagamento para usuários sauditas exibissem uma casa decimal a mais. Embora os dados em si não estivessem incorretos, o atendimento ao cliente recebeu mais de 200 reclamações, e três usuários cancelaram seus pedidos como resultado.
Para piorar a situação:
- A equipe não realizou nenhum teste neste componente.
- O teste manual foi aprovado após apenas dois cliques no Chrome
- Este bug também não foi detectado durante a revisão de código
Alice estava determinada: ela precisava estabelecer um sistema de testes para evitar que esse tipo de problema acontecesse novamente.
(2) Solução com Vitest + Testing Library
Alice introduziu a stack de testes Vitest no projeto:
npm install -D vitest @testing-library/react @testing-library/jest-dom @vitejs/plugin-react msw
Então criou o primeiro teste:
import { render, screen } from '@testing-library/react'
import { OrderSummary } from './OrderSummary'
it('exibe o preço formatado corretamente', () => {
render(<OrderSummary total={99.99} currency="SAR" />)
expect(screen.getByText(/99\.99/)).toBeInTheDocument()
})
(3) Resultados
| Dimensão | Antes | Depois |
|---|---|---|
| Cobertura de Código | < 5% | > 75% |
| Teste de regressão pré-lançamento | Nenhum | Executa automaticamente em 5 minutos |
| Taxa de Bugs em Produção | 8–12 por mês | 0–2 por mês |
| Confiança na Entrega de Novas Funcionalidades | Baixa | Alta |
3. Configurando o Ambiente de Testes Vitest
Vitest é o framework de testes nativo do ecossistema Vite e é nativamente compatível com Next.js 16 (que usa Turbopack e Vite internamente).
graph TB
A[vitest.config.ts] --> B[Plugin React<br/>@vitejs/plugin-react]
A --> C[Globais de Teste<br/>globals:true]
A --> D[Ambiente<br/>jsdom]
A --> E[Arquivos de Setup<br/>setup-test.ts]
E --> F[Matcher jest-dom]
E --> G[Inicialização MSW]
style A fill:#cce5ff
style F fill:#d4edda
style G fill:#d4edda
| Arquivo de Configuração | Propósito | Opções Principais |
|---|---|---|
vitest.config.ts |
Configuração Principal de Teste | environment: 'jsdom' Simula Navegador |
setup-test.ts |
Inicialização Global | Importa jest-dom, inicia o serviço MSW |
tsconfig.json |
Suporte a Tipos | types: ['vitest/globals'] |
(1) Configurar 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: './src/__tests__/setup-test.ts',
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
thresholds: {
statements: 70,
branches: 60,
functions: 70,
lines: 70
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
(2) Arquivo de Setup Global
// src/__tests__/setup-test.ts
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'
afterEach(() => {
cleanup()
})
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
refresh: vi.fn(),
back: vi.fn(),
forward: vi.fn()
}),
usePathname: () => '/',
useSearchParams: () => new URLSearchParams()
}))
vi.mock('react-dom', async () => {
const actual = await vi.importActual('react-dom')
return { ...actual, useFormState: vi.fn() }
})
▶ Exemplo: Verificando que o Vitest está funcionando
// src/__tests__/basic.test.ts
import { render, screen } from '@testing-library/react'
function Hello({ name }: { name: string }) {
return <h1>Olá, {name}!</h1>
}
it('renderiza mensagem de saudação', () => {
render(<Hello name="Alice" />)
expect(screen.getByText('Olá, Alice!')).toBeInTheDocument()
})
npx vitest run
✓ src/__tests__/basic.test.ts (1 test) 12ms
Test Files 1 passed (1)
Tests 1 passed (1)
Saída:
✓ src/__tests__/basic.test.ts (1 test) 12ms
Test Files 1 passed (1)
Tests 1 passed (1)
▶ Exemplo: Adicionando script de teste ao package.json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
Saída:
Estrutura JSON definindo três scripts npm: "test" (vitest run), "test:watch" (vitest) e "test:coverage" (vitest run --coverage).
4. Teste de Componentes com @testing-library/react
A filosofia central da Testing Library: Teste o que os usuários veem e com o que interagem, em vez de detalhes de implementação.
(1) Consultas com "render" e "screen"
graph LR
A[render Componentes] --> B[screen Busca]
B --> C{Tipo de Consulta}
C --> D[getByText Texto]
C --> E[getByRole Semântica]
C --> F[getByTestId ID de Teste]
C --> G[getByPlaceholderText Placeholder]
D --> H[Asserção expect]
E --> H
F --> H
G --> H
| Método de Consulta | Cenários Aplicáveis | Exemplo |
|---|---|---|
getByText |
Conteúdo de texto | getByText('Enviar Pedido') |
getByRole |
Elementos Semânticos | getByRole('button', { name: /Enviar/i }) |
getByPlaceholderText |
Dica de Campo de Entrada | getByPlaceholderText('Digite o email') |
getByTestId |
Elemento não semântico | getByTestId('total-pedido') |
queryByText |
Sem asserções | expect(queryByText('Erro')).not.toBeInTheDocument() |
(2) Testando Componentes Server (Renderização Síncrona)
Componentes Server não exigem JavaScript no lado do cliente; você pode fazer asserções diretamente na saída HTML durante o teste:
// src/app/products/page.tsx
async function ProductsPage() {
const res = await fetch('https://api.example.com/products')
const products = await res.json()
return (
<ul>
{products.map((p: { id: number; name: string; price: number }) => (
<li key={p.id} data-testid="product-item">
{p.name} — R$ {p.price}
</li>
))}
</ul>
)
}
export default ProductsPage
// src/__tests__/products-page.test.tsx
import { render, screen } from '@testing-library/react'
import ProductsPage from '@/app/products/page'
// Mock do fetch global
global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve([
{ id: 1, name: 'iPhone 16', price: 999 },
{ id: 2, name: 'Samsung S26', price: 899 }
])
})
it('renderiza lista de produtos do servidor', async () => {
const page = await ProductsPage()
render(page)
expect(screen.getByText('iPhone 16')).toBeInTheDocument()
expect(screen.getByText('Samsung S26')).toBeInTheDocument()
expect(screen.getAllByTestId('product-item')).toHaveLength(2)
})
▶ Exemplo: Testando Componentes Client (Incluindo Interações do Usuário)
Saída:
Renderiza a UI do componente.
// src/components/Counter.tsx
'use client'
import { useState } from 'react'
export function Counter({ initial = 0 }) {
const [count, setCount] = useState(initial)
return (
<div>
<p data-testid="count">Contagem: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Incrementar</button>
<button onClick={() => setCount(c => c - 1)}>Decrementar</button>
</div>
)
}
Saída:
Um componente interativo com gerenciamento de estado.
Texto visível: Contagem: {count}
// src/__tests__/counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Counter } from '@/components/Counter'
describe('Counter', () => {
it('renderiza com valor inicial', () => {
render(<Counter initial={5} />)
expect(screen.getByTestId('count')).toHaveTextContent('Contagem: 5')
})
it('incrementa ao clicar no botão', () => {
render(<Counter initial={0} />)
fireEvent.click(screen.getByText('Incrementar'))
expect(screen.getByTestId('count')).toHaveTextContent('Contagem: 1')
})
it('decrementa ao clicar no botão', () => {
render(<Counter initial={10} />)
fireEvent.click(screen.getByText('Decrementar'))
expect(screen.getByTestId('count')).toHaveTextContent('Contagem: 9')
})
})
5. Matchers Personalizados no jest-dom
O jest-dom fornece matchers de asserção DOM semânticos, tornando o código de teste mais próximo da linguagem natural.
| Matcher | Função | Exemplo |
|---|---|---|
toBeInTheDocument() |
Elementos no DOM | expect(el).toBeInTheDocument() |
toHaveTextContent(text) |
Correspondência de Conteúdo de Texto | expect(el).toHaveTextContent('Olá') |
toBeVisible() |
Elemento Visível | expect(el).toBeVisible() |
toBeDisabled() |
Botão desabilitado | expect(btn).toBeDisabled() |
toHaveClass(cls) |
Correspondência de classe CSS | expect(el).toHaveClass('active') |
toHaveAttribute(attr) |
Correspondência de Atributo | expect(input).toHaveAttribute('type', 'email') |
toHaveValue(val) |
Correspondência de Valor de Formulário | expect(input).toHaveValue('teste@exemplo.com') |
▶ Exemplo: Teste de Validação de Formulário
Saída:
Renderiza a UI do componente.
import { render, screen, fireEvent } from '@testing-library/react'
import { LoginForm } from '@/components/LoginForm'
describe('Validação do LoginForm', () => {
it('exibe erro para email vazio', () => {
render(<LoginForm />)
fireEvent.click(screen.getByRole('button', { name: /Entrar/i }))
expect(screen.getByText(/Por favor, insira seu endereço de email/i)).toBeInTheDocument()
expect(screen.getByText(/Por favor, insira seu endereço de email/i)).toBeVisible()
})
it('desabilita o botão enviar durante o carregamento', () => {
render(<LoginForm />)
fireEvent.change(screen.getByPlaceholderText('Digite seu endereço de email'), {
target: { value: 'alice@exemplo.com' }
})
fireEvent.click(screen.getByRole('button', { name: /Entrar/i }))
expect(screen.getByRole('button', { name: /Entrando.../i })).toBeDisabled()
})
it('limpa o erro após entrada válida', () => {
render(<LoginForm />)
fireEvent.click(screen.getByRole('button', { name: /Entrar/i }))
expect(screen.getByText(/Por favor, insira seu endereço de email/i)).toBeInTheDocument()
fireEvent.change(screen.getByPlaceholderText('Digite seu endereço de email'), {
target: { value: 'alice@exemplo.com' }
})
expect(screen.queryByText(/Por favor, insira seu endereço de email/i)).not.toBeInTheDocument()
})
})
Saída:
Renderiza a UI do componente ▶ Exemplo: Teste de Validação de Formulário conforme descrito na seção.
6. Testando Server Actions (Mock do Prisma + revalidatePath)
Server Actions precisam simular operações de banco de dados e funções de invalidação de cache.
graph TB
A[Testar Server Action] --> B[Mock do Prisma Client]
A --> C[Mock do revalidatePath]
A --> D[Mock do redirect]
B --> E[Retornar Dados Simulados]
C --> F[Verificar número de chamadas/Parâmetros]
D --> G[Verificar Caminho de Redirecionamento]
style A fill:#cce5ff
style B fill:#d4edda
style C fill:#fff3cd
(1) Ferramenta de Mock do Prisma
// src/__tests__/utils/mock-prisma.ts
import { vi } from 'vitest'
export function createMockPrisma() {
return {
task: {
findMany: vi.fn().mockResolvedValue([
{ id: '1', title: 'Tarefa de Teste', status: 'TODO', projectId: 'p1' }
]),
create: vi.fn().mockImplementation(({ data }) => Promise.resolve({
id: 'novo-id',
...data,
createdAt: new Date()
})),
update: vi.fn().mockImplementation(({ data }) => Promise.resolve(data)),
delete: vi.fn().mockResolvedValue({ id: 'id-excluido' })
},
project: {
findUnique: vi.fn().mockResolvedValue({
id: 'p1',
name: 'Projeto de Teste',
tasks: []
})
},
$transaction: vi.fn().mockImplementation((cb) => cb(createMockPrisma()))
} as any
}
(2) Exemplo de Teste de uma Server Action
// src/actions/task.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'
const taskSchema = z.object({
title: z.string().min(1, 'O título não pode ficar em branco.'),
projectId: z.string().min(1),
status: z.enum(['TODO', 'IN_PROGRESS', 'DONE'])
})
export async function createTask(formData: FormData) {
const data = Object.fromEntries(formData)
const parsed = taskSchema.safeParse(data)
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors }
}
await prisma.task.create({ data: parsed.data })
revalidatePath(`/projects/${parsed.data.projectId}`)
redirect(`/projects/${parsed.data.projectId}`)
}
▶ Exemplo: Teste Completo de Server Action
Saída:
Valida a entrada com schema Zod antes de processar.
Grava no banco de dados e atualiza o cache da página afetada.
Retorna objeto de erro se a validação ou processamento falhar.
// src/__tests__/actions/task.test.ts
import { createTask } from '@/actions/task'
import { createMockPrisma } from '../utils/mock-prisma'
import { vi, describe, it, expect, beforeEach } from 'vitest'
vi.mock('@/lib/prisma', () => ({ prisma: createMockPrisma() }))
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/navigation', () => ({ redirect: vi.fn() }))
describe('createTask', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('cria uma tarefa com sucesso', async () => {
const formData = new FormData()
formData.append('title', 'Escrever Documentação de Teste')
formData.append('projectId', 'p1')
formData.append('status', 'TODO')
const result = await createTask(formData)
expect(result).toBeUndefined()
})
it('retorna erro de validação para título vazio', async () => {
const formData = new FormData()
formData.append('title', '')
formData.append('projectId', 'p1')
formData.append('status', 'TODO')
const result = await createTask(formData)
expect(result).toHaveProperty('error')
expect(result.error.title).toBeDefined()
})
it('chama revalidatePath após a criação', async () => {
const formData = new FormData()
formData.append('title', 'Nova Tarefa')
formData.append('projectId', 'p1')
formData.append('status', 'TODO')
await createTask(formData)
const { revalidatePath } = await import('next/cache')
expect(revalidatePath).toHaveBeenCalledWith('/projects/p1')
})
})
Saída:
Cria um novo registro no banco de dados.
7. MSW Mock de API no Lado do Servidor
MSW (Mock Service Worker) intercepta requisições de rede, permitindo testar a lógica de recuperação de dados sem precisar iniciar o backend real.
graph LR
A[Componente Inicia fetch] --> B[MSW Service Worker]
B --> C{Correspondência de Requisição}
C -->|Handler Correspondente| D[Retorna Dados Mock]
C -->|Sem Correspondência| E[Passa para a rede real]
style B fill:#cce5ff
style D fill:#d4edda
| Conceito | Descrição | Exemplo |
|---|---|---|
| Handler | Manipulador de requisição | http.get('/api/products', resolver) |
| Resolver | Retorna uma resposta mock | return HttpResponse.json([...]) |
| Server | Servidor mock Node.js | setupServer(...handlers) |
| Browser | MSW no navegador | setupWorker(...handlers) |
(1) Definindo Handlers Mock
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
const API_BASE = 'https://api.example.com'
export const handlers = [
// GET /api/products
http.get(`${API_BASE}/products`, () => {
return HttpResponse.json([
{ id: 1, name: 'iPhone 16', price: 999, category: 'Eletrônicos' },
{ id: 2, name: 'Samsung S26', price: 899, category: 'Eletrônicos' }
])
}),
// POST /api/orders
http.post(`${API_BASE}/orders`, async ({ request }) => {
const body = await request.json()
return HttpResponse.json({
id: 'pedido-123',
...body as any,
status: 'confirmado',
createdAt: new Date().toISOString()
})
}),
// GET /api/users/:id
http.get(`${API_BASE}/users/:id`, ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'Alice Wang',
email: 'alice@exemplo.com',
role: 'admin'
})
}),
// Erro 500 para testar error boundaries
http.get(`${API_BASE}/errors/internal`, () => {
return HttpResponse.json(
{ error: 'Erro Interno do Servidor' },
{ status: 500 }
)
})
]
(2) Integração no Setup de Teste
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// src/__tests__/setup-test.ts (Versão Completa)
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, afterAll, beforeAll, vi } from 'vitest'
import { server } from '@/mocks/server'
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
afterEach(() => { cleanup(); server.resetHandlers() })
afterAll(() => server.close())
// Mock next/navigation
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(), replace: vi.fn(),
refresh: vi.fn(), back: vi.fn(), forward: vi.fn()
}),
usePathname: () => '/',
useSearchParams: () => new URLSearchParams()
}))
▶ Exemplo: Testando a Interceptação de API com MSW
Saída:
Módulo TypeScript executado com sucesso.
// src/__tests__/api-integration.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '@/mocks/server'
import ProductsPage from '@/app/products/page'
describe('ProductsPage com MSW', () => {
it('renderiza produtos da API mockada', async () => {
const page = await ProductsPage()
render(page)
expect(screen.getByText('iPhone 16')).toBeInTheDocument()
expect(screen.getByText('Samsung S26')).toBeInTheDocument()
})
it('trata lista de produtos vazia', async () => {
server.use(
http.get('https://api.example.com/products', () => {
return HttpResponse.json([])
})
)
const page = await ProductsPage()
render(page)
await waitFor(() => {
expect(screen.queryByTestId('product-item')).not.toBeInTheDocument()
})
})
it('exibe erro em caso de falha da API', async () => {
server.use(
http.get('https://api.example.com/products', () => {
return HttpResponse.json(
{ error: 'Serviço indisponível' },
{ status: 503 }
)
})
)
await expect(ProductsPage()).rejects.toThrow()
})
})
Saída:
Renderiza a UI do componente ▶ Exemplo: Testando a Interceptação de API com MSW conforme descrito na seção.
8. Exemplo Completo: Teste Full-Stack de um App de Tarefas
// src/__tests__/todo-comprehensive.test.ts
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '@/mocks/server'
import { createMockPrisma } from './utils/mock-prisma'
import { describe, it, expect, vi, beforeEach } from 'vitest'
// ============================================
// Teste Abrangente: Componentes + Action + API do App de Tarefas
// ============================================
// --- 1. Mock de Dependências ---
vi.mock('@/lib/prisma', () => ({ prisma: createMockPrisma() }))
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/navigation', () => ({
redirect: vi.fn(),
useRouter: () => ({ push: vi.fn(), refresh: vi.fn() })
}))
// --- 2. Componentes de Tarefas ---
function TodoItem({ id, title, done, onToggle }: {
id: string; title: string; done: boolean
onToggle: (id: string) => void
}) {
return (
<div data-testid="todo-item">
<span style={{ textDecoration: done ? 'line-through' : 'none' }}>
{title}
</span>
<button onClick={() => onToggle(id)}>
{done ? 'Desfazer' : 'Concluir'}
</button>
</div>
)
}
function TodoList({ todos }: { todos: Array<{ id: string; title: string; done: boolean }> }) {
return (
<div>
{todos.map(t => (
<TodoItem key={t.id} {...t} onToggle={(id) => {
const idx = todos.findIndex(t => t.id === id)
todos[idx].done = !todos[idx].done
}} />
))}
</div>
)
}
// --- 3. Casos de Teste ---
describe('App de Tarefas', () => {
const mockTodos = [
{ id: '1', title: 'Aprender testes no Next.js', done: false },
{ id: '2', title: 'Escrever testes unitários', done: true },
{ id: '3', title: 'Configurar pipeline CI', done: false }
]
it('renderiza todas as tarefas', () => {
render(<TodoList todos={mockTodos} />)
expect(screen.getAllByTestId('todo-item')).toHaveLength(3)
expect(screen.getByText('Aprender testes no Next.js')).toBeInTheDocument()
})
it('exibe tachado para itens concluídos', () => {
render(<TodoList todos={mockTodos} />)
const doneItem = screen.getByText('Escrever testes unitários')
expect(doneItem).toHaveStyle('text-decoration: line-through')
})
it('alterna tarefa ao clicar no botão', () => {
render(<TodoList todos={mockTodos} />)
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[0])
expect(buttons[0]).toHaveTextContent('Desfazer')
})
it('possui rótulos de botão corretos', () => {
render(<TodoList todos={mockTodos} />)
const buttons = screen.getAllByRole('button')
expect(buttons[0]).toHaveTextContent('Concluir')
expect(buttons[1]).toHaveTextContent('Desfazer')
})
})
// --- 4. Teste de API com MSW ---
describe('Mock da API de Tarefas', () => {
it('busca tarefas da API mockada', async () => {
server.use(
http.get('https://api.example.com/todos', () => {
return HttpResponse.json(mockTodos)
})
)
const res = await fetch('https://api.example.com/todos')
const data = await res.json()
expect(data).toHaveLength(3)
expect(data[0].title).toBe('Aprender testes no Next.js')
})
it('trata erro da API com elegância', async () => {
server.use(
http.get('https://api.example.com/todos', () => {
return HttpResponse.json(null, { status: 500 })
})
)
const res = await fetch('https://api.example.com/todos')
expect(res.status).toBe(500)
})
})
✓ src/__tests__/todo-comprehensive.test.ts (7 tests) 45ms
Test Files 1 passed (1)
Tests 7 passed (7)
❓ Perguntas Frequentes
P: Qual é a diferença entre Vitest e Jest? R: O Vitest compartilha sua configuração e ecossistema de plugins com o Vite, inicia de 10 a 20 vezes mais rápido que o Jest (com suporte nativo a ESM) e tem uma API de sintaxe compatível com o Jest (a API globals suporta
expect/describe/it). O Next.js 16 usa Turbopack (parte do ecossistema Vite) internamente, então o Vitest é a escolha mais natural.
P: Como testo um Componente Server que usa
cookies()ouheaders()? R: Essas funções dependem do contexto de requisição do Next.js, que precisa ser mockado nos testes. Recomendamos extrair a lógica de recuperação de dados para uma Server Action ou camada de API separada e, em seguida, testar a renderização do componente e a lógica de negócio separadamente.
P: Qual é a diferença entre MSW e mock manual de fetch? R: O MSW intercepta requisições de rede no nível do Service Worker, permitindo testar cadeias de chamadas fetch reais sem fazer alterações no código do componente. Mockar
global.fetchmanualmente é simples, mas não consegue lidar com correspondência complexa de requisições, serialização de respostas ou cenários de erro.
P: Qual é um limite razoável para cobertura de testes? R: Recomendamos uma abordagem em fases: Defina a meta em 50% para a Fase 1 (componentes principais + server actions), aumente para 70% na Fase 2 (cobrindo branches e casos de borda) e busque 80%+ para projetos em produção. Lembre-se de que a cobertura não é o objetivo final; caminhos críticos (pagamento/login/gravação de dados) devem ter cobertura de 100%.
P: Como testo um formulário que usa
useActionState? R:useActionStatedepende deuseFormStateno React 19, então você precisa mockaruseFormStatenoreact-domdurante os testes. Recomendamos usarfireEvent.submitpara disparar o envio do formulário e, em seguida, fazer asserções sobre o estado da UI após o envio (mensagens de sucesso/erro).
P: Qual é a diferença entre
fireEventeuserEventna Testing Library? R:fireEventdispara eventos DOM diretamente (como click), enquantouserEventsimula interações de usuário mais realistas (como entrada de teclado e gerenciamento de foco). Recomendamos usaruserEventpara testes em produção (pois imita melhor a experiência do usuário) efireEventpara testes unitários (pois é mais leve).
📖 Resumo
- Vitest é o framework de testes preferido para Next.js 16. Ao configurar
vitest.config.ts, você deve especificarenvironment: 'jsdom'e o plugin React. @testing-library/reactfornece as APIsrenderescreen, focando no comportamento visível ao usuário em vez de detalhes de implementação- Os matchers do jest-dom (
toBeInTheDocument,toHaveTextContent,toBeVisible) tornam as asserções de teste mais significativas - O teste de Server Actions requer mock do Prisma Client,
revalidatePatheredirectpara verificar a gravação de dados e a invalidação de cache - O MSW intercepta requisições de rede na camada do Service Worker, sendo adequado para isolar APIs externas em testes de integração
setup-test.tsé um arquivo-chave na infraestrutura de testes que gerencia centralizadamente mocks e inicialização global
📝 Exercícios
-
Exercício Básico (⭐): Crie um
vitest.config.tsque inclua o plugin React, o ambiente jsdom e um alias de caminho para@/, depois escreva o teste mais simplesrender(<div>Olá</div>)para verificar se o matcher Jest-DOM está funcionando corretamente. -
Exercício Avançado (⭐⭐): Escreva testes abrangentes para as Server Actions do seu projeto (como
createUserousubmitOrder): mocke o métodocreatedo Prisma, verifique serevalidatePathé chamado e teste a resposta de erro quando a validação do Zod falha. -
Desafio (⭐⭐⭐): Use o MSW para construir três handlers mock (GET /api/tasks, POST /api/tasks, DELETE /api/tasks/:id) e, em seguida, escreva pelo menos 8 casos de teste para o componente TaskList, que inclui funcionalidade de recuperação, criação e exclusão de dados (incluindo carregamento inicial, lista vazia e tratamento de erros).