Pular para o conteúdo principal

Guia de Testes

Documentação completa da estratégia de testes do LJ Velas e Aromas.


Sumário

  1. Visão geral
  2. Testes unitários e de integração (Jest)
  3. Testes de componentes (React Testing Library)
  4. Testes E2E (Playwright)
  5. Integração Contínua
  6. Cobertura de código
  7. 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

CamadaFrameworkSuitesTestesVelocidade
Unitários + IntegraçãoJest12163~15s
ComponentesJest + RTL(incluso acima)29~15s
E2EPlaywright322~2-5min
Total15185

Contas de teste (seed obrigatório)

Todos os testes E2E dependem do banco populado com o seed:

PapelE-mailSenha
Administradoradmin@ljvemasearomas.com.bradmin123
Clientecliente@teste.comcliente123

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/navigationuseRouter, 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:

  1. getByRole — preferido para botões, inputs, headings, etc.
  2. getByLabelText — para inputs com labels associadas
  3. getByText — para conteúdo textual
  4. getByTestId — último recurso, use data-testid apenas 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 develop ou main → 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

  1. Acesse a aba Actions do repositório
  2. Clique no workflow desejado
  3. Em caso de falha, clique no job com x para ver os logs
  4. 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étricaThresholdO que significa
Statements70%% de declarações executadas
Branches70%% de caminhos condicionais testados
Functions70%% de funções chamadas
Lines70%% 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