Guia de Testes
Documentação completa da estratégia de testes do LJ Velas e Aromas.
Sumário
- Visão geral
- Testes unitários e de integração (Jest)
- Testes de componentes (React Testing Library)
- Testes E2E (Playwright)
- Integração Contínua
- Cobertura de código
- Boas práticas
Visão geral
O projeto adota uma pirâmide de testes com três camadas complementares:
/\
/ \
/ E2E \ 22 testes Playwright
/ (lento)\ Fluxos de usuário completos
/────────────\
/ \
/ Componentes \ 29 testes RTL
/ (médio) \ Comportamento visual e interativo
/────────────────────\
/ \
/ Unitários + Integração\ 163 testes Jest
/ (rápido) \ Lógica de negócio e APIs
/────────────────────────────\
Inventário atual
| Camada | Framework | Suites | Testes | Velocidade |
|---|---|---|---|---|
| Unitários + Integração | Jest | 12 | 163 | ~15s |
| Componentes | Jest + RTL | (incluso acima) | 29 | ~15s |
| E2E | Playwright | 3 | 22 | ~2-5min |
| Total | 15 | 185 |
Contas de teste (seed obrigatório)
Todos os testes E2E dependem do banco populado com o seed:
| Papel | Senha | |
|---|---|---|
| Administrador | admin@ljvemasearomas.com.br | admin123 |
| Cliente | cliente@teste.com | cliente123 |
Testes unitários e de integração (Jest)
Setup e configuração
Arquivo de configuração: jest.config.ts
// jest.config.ts — visão geral das opções relevantes
{
preset: 'next/jest', // integração nativa com Next.js
testEnvironment: 'jsdom', // ambiente de browser simulado
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1', // alias @ para a raiz do projeto
},
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
}
Arquivo de setup: jest.setup.ts
Configurações aplicadas globalmente a todos os testes:
@testing-library/jest-dom— matchers adicionais (.toBeInTheDocument(),.toHaveValue(), etc.)- Mock de
next/navigation—useRouter,usePathname,useSearchParams - Mock de
next/image— renderiza como<img>simples para evitar erros de otimização - Mock de
framer-motion— renderiza componentes sem animações (evita timers e RAF)
Variáveis de ambiente: .env.test
Valores isolados usados apenas nos testes. Nunca use credenciais reais aqui.
Estrutura dos testes
__tests__/
├── lib/ # Lógica de negócio pura
│ ├── utils.test.ts # Formatadores, helpers gerais
│ ├── shipping.test.ts # Cálculo de frete
│ └── webhook-dispatcher.test.ts # Despacho de webhooks
│
├── api/ # Route Handlers do Next.js
│ ├── auth/
│ │ └── login.test.ts # POST /api/auth/login
│ ├── products.test.ts # GET/POST /api/products
│ ├── orders.test.ts # GET/POST /api/orders
│ ├── webhooks/
│ │ └── stock.test.ts # Webhook de atualização de estoque
│ └── shipping/
│ └── calculate.test.ts # POST /api/shipping/calculate
│
└── components/ # Componentes React
├── CartDrawer.test.tsx
├── ShippingCalculator.test.tsx
├── CouponInput.test.tsx
└── ProductReviews.test.tsx
Rodando os testes
# Rodar todos os testes uma vez
npm test
# Modo watch (reexecuta ao salvar arquivos)
npm run test:watch
# CI (com cobertura, sem watch, força saída)
npm run test:ci
# Rodar apenas um arquivo específico
npm test -- __tests__/lib/utils.test.ts
# Rodar testes que correspondem a um padrão
npm test -- --testNamePattern="deve calcular frete"
# Ver cobertura no terminal
npm test -- --coverage
Mocks configurados
next/navigation
Disponível globalmente via jest.setup.ts. Você pode sobrescrever por teste:
// __tests__/components/MeuComponente.test.tsx
import { useRouter } from 'next/navigation';
const mockPush = jest.fn();
(useRouter as jest.Mock).mockReturnValue({
push: mockPush,
pathname: '/produtos',
query: {},
});
test('navega ao clicar no botão', async () => {
render(<MeuComponente />);
await userEvent.click(screen.getByRole('button', { name: /ver produto/i }));
expect(mockPush).toHaveBeenCalledWith('/produtos/123');
});
framer-motion
Todos os componentes motion.* são substituídos pelos equivalentes HTML simples. AnimatePresence renderiza os filhos diretamente.
// Não é necessário configurar nada — o mock já está em jest.setup.ts
// Os componentes renderizam sem animação, mas com o mesmo conteúdo
render(<CartDrawer isOpen={true} />);
expect(screen.getByRole('dialog')).toBeInTheDocument();
Prisma Client
Para testes de API, use mocks manuais do Prisma:
// __tests__/api/products.test.ts
jest.mock('@/lib/prisma', () => ({
product: {
findMany: jest.fn(),
create: jest.fn(),
},
}));
import prisma from '@/lib/prisma';
test('retorna lista de produtos', async () => {
const mockProducts = [{ id: 1, name: 'Vela Lavanda', price: 2990 }];
(prisma.product.findMany as jest.Mock).mockResolvedValue(mockProducts);
const response = await GET(new Request('http://localhost/api/products'));
const data = await response.json();
expect(data.products).toHaveLength(1);
expect(data.products[0].name).toBe('Vela Lavanda');
});
Escrevendo novos testes
Teste de função utilitária
// __tests__/lib/minha-funcao.test.ts
import { formatPrice } from '@/lib/utils';
describe('formatPrice', () => {
it('formata centavos para reais com símbolo', () => {
expect(formatPrice(2990)).toBe('R$ 29,90');
});
it('formata zero corretamente', () => {
expect(formatPrice(0)).toBe('R$ 0,00');
});
it('lida com valores grandes', () => {
expect(formatPrice(100000)).toBe('R$ 1.000,00');
});
});
Teste de Route Handler
// __tests__/api/minha-rota.test.ts
import { POST } from '@/app/api/minha-rota/route';
jest.mock('@/lib/prisma', () => ({ ... }));
jest.mock('@/lib/auth', () => ({
getSessionFromRequest: jest.fn().mockResolvedValue({
userId: 1,
role: 'CUSTOMER',
}),
}));
describe('POST /api/minha-rota', () => {
it('retorna 201 para payload válido', async () => {
const req = new Request('http://localhost/api/minha-rota', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ campo: 'valor' }),
});
const response = await POST(req);
expect(response.status).toBe(201);
});
it('retorna 400 para payload inválido', async () => {
const req = new Request('http://localhost/api/minha-rota', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}), // campo obrigatório ausente
});
const response = await POST(req);
expect(response.status).toBe(400);
});
});
Thresholds de cobertura
O projeto exige 70% de cobertura em todas as métricas (branches, functions, lines, statements). O CI falha se qualquer métrica ficar abaixo desse limite.
Para verificar localmente:
npm run test:ci
# Relatório HTML em: coverage/lcov-report/index.html
Testes de componentes (React Testing Library)
Padrões utilizados
O projeto segue as boas práticas da RTL, priorizando queries que refletem como o usuário interage com a interface:
getByRole— preferido para botões, inputs, headings, etc.getByLabelText— para inputs com labels associadasgetByText— para conteúdo textualgetByTestId— último recurso, usedata-testidapenas quando necessário
// Prefira:
screen.getByRole('button', { name: /adicionar ao carrinho/i })
screen.getByLabelText(/cep/i)
// Evite:
screen.getByTestId('btn-add-cart') // não testa como o usuário vê
screen.getByClassName('btn-primary') // testa implementação, não comportamento
Exemplos de testes existentes
CartDrawer.test.tsx
// Testa: abertura/fechamento, listagem de itens, remoção, cálculo de total
describe('CartDrawer', () => {
it('exibe os produtos no carrinho', () => {
renderWithStore(<CartDrawer />, {
cartItems: [{ id: 1, name: 'Vela Lavanda', price: 2990, qty: 2 }],
});
expect(screen.getByText('Vela Lavanda')).toBeInTheDocument();
expect(screen.getByText('R$ 59,80')).toBeInTheDocument(); // total
});
it('remove item ao clicar em excluir', async () => {
const { store } = renderWithStore(<CartDrawer />, { cartItems: [...] });
await userEvent.click(screen.getByRole('button', { name: /remover/i }));
expect(store.getState().cart.items).toHaveLength(0);
});
});
ShippingCalculator.test.tsx
// Testa: digitação de CEP, submit, exibição de opções, tratamento de erro
describe('ShippingCalculator', () => {
it('exibe opções de frete após consulta bem-sucedida', async () => {
server.use(
http.post('/api/shipping/calculate', () =>
HttpResponse.json({ options: [{ name: 'PAC', price: 1500, days: 7 }] })
)
);
render(<ShippingCalculator productId={1} />);
await userEvent.type(screen.getByLabelText(/cep/i), '01310100');
await userEvent.click(screen.getByRole('button', { name: /calcular/i }));
expect(await screen.findByText('PAC')).toBeInTheDocument();
expect(screen.getByText('R$ 15,00')).toBeInTheDocument();
});
});
Renderizando com providers
Para componentes que acessam Zustand ou Context:
// __tests__/utils/renderWithStore.tsx
import { render } from '@testing-library/react';
import { createStore } from '@/store';
export function renderWithStore(ui: React.ReactElement, initialState = {}) {
const store = createStore(initialState);
return {
...render(<StoreProvider store={store}>{ui}</StoreProvider>),
store,
};
}
Testes E2E (Playwright)
Setup e pré-requisitos
Configuração: playwright.config.ts
// Visão geral da configuração
{
testDir: './e2e',
baseURL: 'http://localhost:3000',
projects: [
{ name: 'chromium' },
{ name: 'firefox' },
{ name: 'webkit' },
{ name: 'mobile-chrome' },
{ name: 'mobile-safari' },
],
}
Pré-requisito obrigatório: O banco deve estar populado com o seed antes de rodar os testes E2E:
# 1. Banco de dados rodando e com seed
npm run db:seed
# 2. Aplicação rodando
npm run dev
# 3. Em outro terminal: rodar testes E2E
npm run test:e2e
Instalando os browsers do Playwright:
npx playwright install
# Ou apenas os browsers necessários:
npx playwright install chromium firefox webkit
Estrutura dos testes
e2e/
├── helpers/
│ └── auth.ts # Funções auxiliares de autenticação
├── auth.spec.ts # Cadastro, login, logout, guards (10 testes)
├── purchase.spec.ts # Jornada de compra completa (5 testes)
└── admin-products.spec.ts # CRUD de produtos no admin (7 testes)
Helpers de autenticação
// e2e/helpers/auth.ts
import { Page } from '@playwright/test';
export async function loginAsCustomer(page: Page) {
await page.goto('/login');
await page.getByLabel(/e-mail/i).fill('cliente@teste.com');
await page.getByLabel(/senha/i).fill('cliente123');
await page.getByRole('button', { name: /entrar/i }).click();
await page.waitForURL('/');
}
export async function loginAsAdmin(page: Page) {
await page.goto('/login');
await page.getByLabel(/e-mail/i).fill('admin@ljvemasearomas.com.br');
await page.getByLabel(/senha/i).fill('admin123');
await page.getByRole('button', { name: /entrar/i }).click();
await page.waitForURL('/admin');
}
Escrevendo novos testes E2E
Estrutura básica
// e2e/meu-fluxo.spec.ts
import { test, expect } from '@playwright/test';
import { loginAsCustomer } from './helpers/auth';
test.describe('Fluxo de avaliação de produto', () => {
test.beforeEach(async ({ page }) => {
await loginAsCustomer(page);
});
test('cliente pode avaliar produto comprado', async ({ page }) => {
// Navegar até o produto
await page.goto('/produtos/vela-lavanda');
// Interagir
await page.getByRole('button', { name: /avaliar/i }).click();
await page.getByLabel(/nota/i).fill('5');
await page.getByLabel(/comentário/i).fill('Produto excelente!');
await page.getByRole('button', { name: /enviar avaliação/i }).click();
// Verificar resultado
await expect(page.getByText('Avaliação enviada!')).toBeVisible();
await expect(page.getByText('Produto excelente!')).toBeVisible();
});
});
Boas práticas para testes E2E
// Use locators resilientes (preferência: role > label > text > testid)
await page.getByRole('button', { name: /adicionar ao carrinho/i }).click();
// Aguarde elementos aparecerem antes de interagir
await expect(page.getByRole('dialog')).toBeVisible();
// Use waitForURL para confirmar navegação
await page.getByRole('button', { name: /finalizar compra/i }).click();
await page.waitForURL('/checkout/confirmacao');
// Evite sleeps arbitrários
// await page.waitForTimeout(2000); // NÃO faça isso
// Prefira waitForResponse para ações com chamadas de API
const responsePromise = page.waitForResponse('/api/orders');
await page.getByRole('button', { name: /confirmar pedido/i }).click();
await responsePromise;
Debugging
Modo headed (com janela do browser)
# Executar com browser visível
npm run test:e2e -- --headed
# Executar com UI interativa do Playwright
npm run test:e2e:ui
Pausar no momento da falha
# Pausa ao encontrar o primeiro erro (abre inspector)
npm run test:e2e -- --debug
# Adicionar pausas no código do teste
await page.pause(); // adicione esta linha onde quiser pausar
Trace Viewer
O Playwright registra traces em caso de falha. Para visualizar:
# Rodar com traces sempre habilitados
npm run test:e2e -- --trace on
# Abrir o trace de um teste específico
npx playwright show-trace test-results/meu-teste/trace.zip
Relatório HTML
npm run test:e2e:report
# Abre o relatório no browser com screenshots e vídeos das falhas
Rodando em diferentes browsers
# Apenas Chromium (mais rápido)
npm run test:e2e -- --project=chromium
# Firefox
npm run test:e2e -- --project=firefox
# WebKit (Safari)
npm run test:e2e -- --project=webkit
# Mobile Chrome
npm run test:e2e -- --project="mobile-chrome"
# Todos os browsers
npm run test:e2e
Integração Contínua
Workflows
ci.yml — Qualidade de código
Trigger: Push em qualquer branch
# O que roda:
# 1. npm ci (instalação limpa)
# 2. npm run lint (ESLint, zero warnings)
# 3. npx tsc --noEmit (TypeScript check)
tests.yml — Testes automatizados
Trigger:
- Push para
developoumain→ Jest - Pull Request para
main→ Jest + Playwright (Chromium)
# Para Jest:
# 1. npm ci
# 2. npm run test:ci (cobertura, forceExit)
# Artefato: coverage/ (7 dias de retenção)
# Para Playwright (apenas em PRs para main):
# 1. npm ci
# 2. npx playwright install chromium (apenas Chromium no CI)
# 3. npm run build && npm run start & (start em background)
# 4. npm run test:e2e -- --project=chromium
# Artefato: playwright-report/ (7 dias de retenção)
Verificando o status no GitHub
- Acesse a aba Actions do repositório
- Clique no workflow desejado
- Em caso de falha, clique no job com
xpara ver os logs - Faça download dos artefatos (relatórios) se necessário
Cobertura de código
Gerando o relatório
# Gerar e exibir no terminal
npm run test:ci
# O relatório HTML fica em:
# coverage/lcov-report/index.html
Abra o relatório no browser:
# Windows
start coverage/lcov-report/index.html
# macOS
open coverage/lcov-report/index.html
# Linux
xdg-open coverage/lcov-report/index.html
Interpretando o relatório
O relatório mostra cobertura por arquivo, função e linha:
- Verde (≥ 80%): Boa cobertura
- Amarelo (60-79%): Cobertura aceitável, mas pode melhorar
- Vermelho (< 60%): Precisa de atenção
Métricas atuais (thresholds mínimos)
| Métrica | Threshold | O que significa |
|---|---|---|
| Statements | 70% | % de declarações executadas |
| Branches | 70% | % de caminhos condicionais testados |
| Functions | 70% | % de funções chamadas |
| Lines | 70% | % de linhas de código executadas |
Excluindo arquivos da cobertura
Adicione ao jest.config.ts:
collectCoverageFrom: [
'**/*.{ts,tsx}',
'!**/*.d.ts',
'!**/node_modules/**',
'!**/.next/**',
'!**/coverage/**',
'!prisma/seed.js', // seed não precisa de cobertura
'!app/**/layout.tsx', // layouts são testados via E2E
],
Boas práticas
Nomenclatura
Use o padrão deve [comportamento esperado] quando [condição]:
// Bom
it('deve exibir erro quando CEP for inválido')
it('deve redirecionar para login quando não autenticado')
it('deve aplicar desconto quando cupom for válido')
// Evite
it('teste do botão de submit')
it('funciona corretamente')
it('verifica o CEP')
Isolamento
Cada teste deve ser completamente independente:
// Bom: estado resetado antes de cada teste
beforeEach(() => {
jest.clearAllMocks();
mockPrisma.product.findMany.mockResolvedValue([]);
});
// Evite: testes que dependem de execução anterior
test('teste 2 assume que teste 1 criou dados') // Frágil!
Seed e factories
Para criar dados de teste consistentes, use factories simples:
// __tests__/factories/product.ts
export function makeProduct(overrides = {}) {
return {
id: 1,
name: 'Vela Lavanda',
slug: 'vela-lavanda',
price: 2990,
stock: 10,
active: true,
...overrides,
};
}
// Uso nos testes
const product = makeProduct({ stock: 0 });
const expensiveProduct = makeProduct({ price: 9990, name: 'Vela Premium' });
Não testes demais, não testes de menos
- Teste comportamento, não implementação: Se você refatorar sem mudar o comportamento, os testes não devem quebrar.
- Um assert por comportamento: Testes com muitos asserts são difíceis de diagnosticar quando falham.
- Prefira testes de integração a unitários para APIs: Testar o Route Handler completo (com mock do Prisma) é mais valioso do que testar cada função isolada.
Testes de snapshot
Use com moderação — snapshots quebram com qualquer mudança visual e geram ruído no código review:
// Use apenas para verificar estrutura HTML estável
expect(container).toMatchSnapshot();
// Atualizar snapshots quando necessário (mudança intencional)
npm test -- --updateSnapshot
Nunca comite testes pulados
// Não deixe testes desabilitados no código commitado
test.skip('feature ainda não implementada', () => { ... }) // Proibido no main
// Se o teste está falhando por um bug conhecido, abra uma issue
// e delete o teste ou corrija o bug antes do merge