Pular para o conteúdo
desenvolvimento

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.

Douglas M. Pereira5 min de leitura
testes automatizadosjesttestingtddplaywrightqualidade de software

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.