Pular para o conteúdo
B
Bradata
testesqualidadeCI/CDengenharia

Testes automatizados para software empresarial: o que testar, como testar e quando parar

Guia prático de testes automatizados para software enterprise. Unit, integração, E2E, CI/CD. Playwright, Detox, cobertura real.

Por Bradata··9 min de leitura

O bug mais caro é aquele que o cliente encontra

Existe um número que deveria tirar o sono de todo gestor de software enterprise: corrigir um bug em produção custa entre 30x e 100x mais do que corrigir durante o desenvolvimento. Esse dado vem do NIST (National Institute of Standards and Technology) e se mantém consistente há mais de 20 anos de pesquisa.

Em sistemas empresariais, o custo é ainda maior. Um bug na emissão de NF-e que passa despercebido por 3 dias não é só um ticket de suporte. É retrabalho fiscal, carta de correção, risco de multa e um cliente que perde a confiança no sistema. Já vimos isso acontecer. Mais de uma vez.

Testes automatizados não eliminam todos os bugs. Mas eliminam as categorias mais previsíveis e mais frequentes deles. E fazem isso toda vez que alguém executa git push, sem depender de boa vontade, atenção ou memória de ninguém.

As três camadas que importam

A pirâmide de testes não é novidade, mas continuo vendo equipes que a implementam errada. Ou escrevem 500 testes unitários que testam getters e setters e zero testes de integração. Ou escrevem 40 testes E2E frágeis que quebram a cada deploy e ninguém conserta.

Testes unitários: rápidos, baratos, focados

Testes unitários verificam uma unidade isolada de lógica. Uma função, um método, um cálculo. Sem banco de dados, sem rede, sem filesystem.

Onde fazem mais diferença em software empresarial:

  • Cálculos fiscais. ICMS com substituição tributária tem regras que variam por estado, por produto e por regime da empresa. Uma função que calcula ICMS-ST precisa de testes para cada combinação relevante. São 27 estados com regras diferentes. Sem testes unitários, qualquer mudança nessa lógica é roleta russa.
  • Validações de negócio. CPF, CNPJ, inscrição estadual, chave de acesso de NF-e. Cada um tem uma regra de validação específica. Testes unitários garantem que essas regras continuam funcionando quando alguém refatora o código 6 meses depois.
  • Transformações de dados. Parsear um arquivo SPED, converter um XML de NF-e para objeto, formatar um boleto. Funções puras com entrada e saída conhecidas. Perfeitas para teste unitário.

A meta que faz sentido: 80% de cobertura em testes unitários. Não 100%. Buscar 100% gera testes que existem só para satisfazer a métrica, testando código trivial que nunca vai quebrar. Os últimos 20% geralmente são código de infraestrutura (configurações, wiring, adaptadores) onde o custo de testar unitariamente é alto e o valor é baixo.

Framework: para projetos em TypeScript/Node, Vitest tomou o lugar do Jest em 2025-2026 pela velocidade. Para Python, pytest continua imbatível. Para .NET, xUnit.

Testes de integração: onde os bugs de verdade moram

Testes de integração verificam que dois ou mais componentes funcionam juntos. Seu serviço conversa com o banco de dados. Seu módulo de faturamento chama a API do gateway de pagamento. Seu parser de XML valida contra o schema da SEFAZ.

A maioria dos bugs que chegam a produção em software empresarial são bugs de integração. A lógica individual está correta, mas a comunicação entre as partes falha. O formato da data que o frontend envia não é o que o backend espera. A query funciona no PostgreSQL 15 mas quebra no 14. O serviço externo mudou o contrato da API sem avisar.

Como testar banco de dados: containers, não mocks.

Já vi equipes que mocam todas as chamadas ao banco e consideram isso "teste de integração". Não é. Se você não está executando uma query real contra um banco real, você está testando sua capacidade de escrever mocks.

A abordagem que funciona: Docker Compose sobe uma instância do banco (PostgreSQL, MySQL, MongoDB, o que for) antes dos testes e derruba depois. Cada suite de testes roda contra uma database limpa.

# docker-compose.test.yml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: test_db
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    ports:
      - "5433:5432"
    tmpfs:
      - /var/lib/postgresql/data

O tmpfs é o detalhe que importa. Montar o datadir em memória faz os testes rodarem 3x a 5x mais rápido que com disco. Para uma suite de 200 testes de integração, isso é a diferença entre 45 segundos e 3 minutos.

Alternativa para testes mais leves: SQLite in-memory como substituto de PostgreSQL para testes que não dependem de features específicas do banco (CTEs recursivas, JSONB, window functions). Funciona para 60% dos casos. Para os outros 40%, use o banco real.

Meta de cobertura: 60% de integração sobre os módulos que tocam recursos externos (banco, APIs, filesystem). Não sobre o codebase inteiro.

Testes E2E: poucos, estratégicos, nos caminhos críticos

Testes end-to-end simulam o usuário real. Abrem o navegador, clicam em botões, preenchem formulários, verificam resultados. São lentos, caros de manter e frágeis. E absolutamente necessários para os fluxos que não podem quebrar.

Para web: Playwright. Não Selenium, não Cypress. Playwright. As razões são práticas:

  • Suporte nativo a múltiplos browsers (Chromium, Firefox, WebKit) com a mesma API
  • Auto-wait inteligente que elimina 90% dos sleep e waitForElement manuais
  • Trace viewer que grava screenshots, DOM snapshots e network logs de cada etapa do teste
  • Velocidade: roda testes em paralelo por padrão, com isolamento de contexto por browser
// Teste E2E: fluxo de emissão de NF-e
test('deve emitir NF-e com sucesso para venda simples', async ({ page }) => {
  await page.goto('/vendas/nova');
  
  await page.getByLabel('Cliente').fill('Empresa Teste LTDA');
  await page.getByRole('option', { name: /Empresa Teste/ }).click();
  
  await page.getByLabel('Produto').fill('Parafuso M10');
  await page.getByRole('option', { name: /Parafuso M10/ }).click();
  await page.getByLabel('Quantidade').fill('100');
  
  await page.getByRole('button', { name: 'Emitir NF-e' }).click();
  
  await expect(page.getByText('NF-e emitida com sucesso')).toBeVisible();
  await expect(page.getByTestId('nfe-numero')).toHaveText(/^\d{9}$/);
});

Para mobile: Detox. Se o software enterprise tem app React Native (e muitos têm, especialmente apps de força de vendas, gestão de frotas e ponto eletrônico), Detox é a melhor opção para E2E. Diferente do Appium, Detox sincroniza com o loop de animações e a fila de rede do React Native, o que reduz drasticamente os testes flakey.

// Teste E2E mobile: registro de ponto
describe('Registro de ponto', () => {
  it('deve registrar entrada com geolocalização', async () => {
    await device.launchApp({ permissions: { location: 'always' } });
    
    await element(by.id('btn-registrar-ponto')).tap();
    await element(by.id('tipo-entrada')).tap();
    
    await expect(element(by.id('confirmacao-ponto'))).toBeVisible();
    await expect(element(by.id('localizacao-registrada'))).toBeVisible();
  });
});

Meta de cobertura E2E: não meça por porcentagem de código. Meça por fluxos críticos cobertos. Liste os 10 a 15 fluxos que, se quebrarem, o cliente liga em 5 minutos. Teste esses. Exemplos clássicos em software empresarial: login, emissão de documento fiscal, processamento de pagamento, geração de relatório financeiro, cadastro de produto.

CI/CD: testes que não rodam automaticamente não existem

Ter testes escritos e não rodá-los no pipeline é o mesmo que ter extintor de incêndio trancado num armário. A teoria da segurança está lá. A prática, não.

A configuração mínima viável para um projeto enterprise:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run test:unit -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

  integration:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: test_db
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run test:integration

  e2e:
    runs-on: ubuntu-latest
    needs: [unit, integration]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-traces
          path: test-results/

Pontos importantes nessa configuração:

  1. Testes unitários e de integração rodam em paralelo. Não faz sentido esperar um terminar para começar o outro.
  2. E2E roda só depois que os dois passam. Testes E2E são caros (em tempo e em minutos de CI). Não desperdice rodando-os se os testes mais rápidos já falharam.
  3. Traces do Playwright são salvos como artifact em caso de falha. Depurar um teste E2E sem o trace viewer é perda de tempo.

Padrões que aprendemos em produção

Factories, não fixtures estáticos. Manter arquivos JSON com dados de teste é uma armadilha de manutenção. Quando o schema muda, você precisa atualizar dezenas de arquivos. Use factories (como fishery para TypeScript ou factory_boy para Python) que geram dados a partir de defaults e permitem override pontual.

Um banco por suite, não por teste. Criar e destruir um banco para cada teste individual é lento. Crie o banco uma vez, rode os testes com transações que fazem rollback ao final de cada caso. É 10x mais rápido e igualmente isolado.

Teste o contrato, não a implementação. Se sua API retorna { id: 1, nome: "Produto" }, teste que o contrato está correto. Não teste que internamente a query faz um JOIN em 3 tabelas. Implementação muda. Contrato não deveria.

Nomeie testes como comportamento. "deve rejeitar NF-e com CNPJ inválido" é infinitamente melhor que "testValidarCNPJ". Quando o teste falha no CI às 23h, o nome precisa dizer o que quebrou sem que ninguém precise ler o código.

Flaky tests: a praga silenciosa

Testes flakey (que passam e falham aleatoriamente) destroem a confiança no pipeline. Se o time aprende que "às vezes o CI falha sem motivo", começa a ignorar falhas reais. Já vi equipes que configuravam retry 3x como "solução". Isso é curativo, não preventivo.

Regra na Bradata: todo teste flakey é corrigido ou removido em 48 horas. Sem exceção.

As causas mais comuns: testes E2E com waits insuficientes (use auto-waiting do Playwright, não sleep(3000)), testes de integração que compartilham banco sem isolamento de dados, e testes que dependem de horário do sistema. Para este último, mock o clock com vi.useFakeTimers() ou freezegun no Python.

O argumento econômico

Para quem precisa justificar investimento em testes para a diretoria: cada hora investida em testes automatizados economiza entre 3 e 8 horas de debugging e suporte ao longo do ciclo de vida do software. Esse número vem da nossa experiência em 5 anos de projetos enterprise.

Um projeto nosso de ERP com 1.200 testes unitários, 340 de integração e 28 E2E cobrindo os fluxos críticos tem taxa de bugs em produção de 0,3 por sprint. Um projeto similar que herdamos de outro fornecedor, sem testes automatizados, tinha 4,7 bugs por sprint. O custo de correção era 15x maior que o custo de manutenção da suite de testes.

Por onde começar se o projeto não tem nada

Não tente fazer tudo de uma vez. A prioridade:

Semana 1: configure CI com lint e um teste de sanidade. O pipeline precisa existir antes dos testes.

Semanas 2 a 4: escreva testes de integração para os 5 fluxos mais críticos. Aqueles que, se quebrarem, geram ligação do cliente de madrugada.

Mês 2: adicione testes unitários para a camada de lógica de negócio. Comece pelos módulos com mais bugs reportados nos últimos 6 meses.

Mês 3: adicione testes E2E com Playwright para os 10 fluxos principais. Configure para rodar antes de cada deploy.

A partir daí, toda feature nova entra com testes. Código legado ganha cobertura conforme é modificado. Em 6 meses, a cobertura está num nível saudável sem ter parado o desenvolvimento.

Testes automatizados não são custo. São o investimento com maior retorno em engenharia de software. Toda vez que a suite pega um bug antes de chegar a produção, ela se paga mais uma vez.

Precisa de um talento tech agora?

Fale com a Bradata e receba uma proposta em 24 horas úteis.