Testes Automatizados: Estratégia Prática para Times de Desenvolvimento
Como estruturar testes automatizados no seu projeto: pirâmide de testes, unit tests com Jest, integration tests, E2E com Playwright e o que testar de verdade.
Por que a maioria dos times não testa adequadamente
Os motivos têm um padrão: "não temos tempo", "o código não está estruturado para testes", "só testamos o que é crítico". O resultado é regressão frequente, medo de refatorar e deploys que quebram funcionalidades inesperadas.
Testes bem estruturados não atrasam o desenvolvimento — aceleraram após a curva inicial. O problema é que poucos times aprendem a testar de forma estratégica.
A pirâmide de testes
/\
/ \
/ E2E \ ← poucos, lentos, caros
/--------\
/ Integration\ ← razoável quantidade
/--------------\
/ Unit Tests \ ← muitos, rápidos, baratos
/------------------\
Unit tests (70–80%): testam uma função/classe isolada. Rápidos, não dependem de banco ou rede.
Integration tests (15–20%): testam a interação real entre componentes — ex: rota HTTP + banco de dados real.
E2E tests (5–10%): testam fluxos completos via browser. Lentos, mas validam o caminho crítico do usuário.
Unit Tests com Jest
// src/services/order-service.ts
export class OrderService {
constructor(
private readonly orderRepo: OrderRepository,
private readonly inventoryService: InventoryService,
) {}
async createOrder(items: OrderItem[], userId: string): Promise<Order> {
// Verificar disponibilidade de estoque
for (const item of items) {
const available = await this.inventoryService.checkAvailability(
item.productId, item.quantity
)
if (!available) {
throw new Error(`Produto ${item.productId} sem estoque suficiente`)
}
}
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
return this.orderRepo.create({ items, userId, total, status: 'pending' })
}
}
// src/services/order-service.test.ts
import { OrderService } from './order-service'
describe('OrderService', () => {
let service: OrderService
let mockOrderRepo: jest.Mocked<OrderRepository>
let mockInventory: jest.Mocked<InventoryService>
beforeEach(() => {
mockOrderRepo = {
create: jest.fn(),
} as any
mockInventory = {
checkAvailability: jest.fn(),
} as any
service = new OrderService(mockOrderRepo, mockInventory)
})
it('cria pedido quando há estoque disponível', async () => {
const items = [{ productId: 'prod-1', quantity: 2, price: 50 }]
const expectedOrder = { id: 'order-1', items, userId: 'user-1', total: 100, status: 'pending' }
mockInventory.checkAvailability.mockResolvedValue(true)
mockOrderRepo.create.mockResolvedValue(expectedOrder)
const result = await service.createOrder(items, 'user-1')
expect(result).toEqual(expectedOrder)
expect(mockInventory.checkAvailability).toHaveBeenCalledWith('prod-1', 2)
expect(mockOrderRepo.create).toHaveBeenCalledWith({
items, userId: 'user-1', total: 100, status: 'pending'
})
})
it('lança erro quando produto está sem estoque', async () => {
const items = [{ productId: 'prod-sem-estoque', quantity: 1, price: 30 }]
mockInventory.checkAvailability.mockResolvedValue(false)
await expect(service.createOrder(items, 'user-1'))
.rejects
.toThrow('sem estoque suficiente')
expect(mockOrderRepo.create).not.toHaveBeenCalled()
})
})
Integration Tests com banco real
// tests/integration/orders.test.ts
import { app } from '../../src/app'
import { db } from '../../src/db'
import supertest from 'supertest'
import { createTestUser, createTestProduct } from '../factories'
describe('POST /api/orders (integration)', () => {
// Limpar banco antes de cada teste
beforeEach(async () => {
await db.orders.deleteMany()
await db.users.deleteMany()
await db.products.deleteMany()
})
afterAll(async () => {
await db.$disconnect()
})
it('cria pedido e retorna 201 com dados completos', async () => {
const user = await createTestUser()
const product = await createTestProduct({ price: 100, stock: 10 })
const token = generateToken(user.id)
const response = await supertest(app)
.post('/api/orders')
.set('Authorization', `Bearer ${token}`)
.send({
items: [{ productId: product.id, quantity: 2 }]
})
expect(response.status).toBe(201)
expect(response.body).toMatchObject({
id: expect.any(String),
userId: user.id,
total: 200,
status: 'pending',
items: expect.arrayContaining([
expect.objectContaining({ productId: product.id, quantity: 2 })
])
})
// Verificar que o banco foi atualizado
const orderInDb = await db.orders.findUnique({ where: { id: response.body.id } })
expect(orderInDb).not.toBeNull()
expect(orderInDb?.total).toBe(200)
})
})
E2E Tests com Playwright
// tests/e2e/checkout.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Fluxo de checkout', () => {
test.use({ baseURL: process.env.E2E_BASE_URL || 'http://localhost:3000' })
test('usuário consegue completar um pedido', async ({ page }) => {
// Login
await page.goto('/login')
await page.fill('[name=email]', 'teste@exemplo.com')
await page.fill('[name=password]', 'senha123')
await page.click('button[type=submit]')
// Esperar redirecionar ao dashboard
await expect(page).toHaveURL('/dashboard')
// Adicionar produto ao carrinho
await page.goto('/produtos/camiseta-azul')
await page.selectOption('[name=size]', 'M')
await page.click('text=Adicionar ao carrinho')
// Verificar toast de confirmação
await expect(page.getByText('Produto adicionado!')).toBeVisible()
// Ir ao checkout
await page.goto('/checkout')
await expect(page.getByTestId('order-total')).toContainText('R$ 89,90')
// Preencher endereço
await page.fill('[name=cep]', '01310-100')
await page.waitForResponse('**/api/cep/**') // aguarda preenchimento automático
await page.click('text=Finalizar pedido')
await expect(page).toHaveURL(/\/pedido\/\w+\/confirmacao/)
await expect(page.getByText('Pedido confirmado!')).toBeVisible()
})
})
Jest config para projetos TypeScript
// jest.config.ts
import type { Config } from 'jest'
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
// Separar unit de integration
projects: [
{
displayName: 'unit',
testMatch: ['<rootDir>/src/**/*.test.ts'],
setupFilesAfterEach: ['<rootDir>/tests/setup.ts'],
},
{
displayName: 'integration',
testMatch: ['<rootDir>/tests/integration/**/*.test.ts'],
globalSetup: '<rootDir>/tests/global-setup.ts',
globalTeardown: '<rootDir>/tests/global-teardown.ts',
}
],
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
}
}
}
export default config
O que testar (e o que não testar)
Teste obrigatório:
- Lógica de negócio complexa (cálculos, regras, fluxos)
- Tratamento de erros e edge cases
- Autorização (usuário A não acessa dados de B)
- Todas as integrações externas (APIs, banco, filas)
Não vale o custo:
- Getters/setters triviais
- Código que é pura delegação sem lógica
- Funções utilitárias óbvias (format, parse simples)
- Testes que testam o framework, não o seu código
A cobertura de 100% não é o objetivo — testes que pegam bugs reais e dão confiança para refatorar são o objetivo.
