Como automatizar testes em aplicações web

1. Introdução à automação de testes web

A automação de testes em aplicações web deixou de ser um diferencial para se tornar uma necessidade em projetos de qualquer escala. Economia de tempo é o benefício mais imediato: um conjunto de testes automatizados pode executar centenas de cenários em minutos, enquanto testes manuais consumiriam horas ou dias. A consistência é outro pilar — testes automatizados executam exatamente as mesmas ações a cada execução, eliminando erros humanos por cansaço ou distração. Já a cobertura permite validar fluxos complexos e casos de borda que seriam impraticáveis manualmente.

A diferença fundamental entre testes manuais e automatizados reside na reprodutibilidade e no custo de execução. Testes manuais são ideais para exploração inicial e validação de usabilidade, enquanto testes automatizados brilham em regressões contínuas. As camadas de teste seguem a pirâmide clássica: testes unitários na base (rápidos e isolados), testes de integração no meio (validando interações entre componentes) e testes funcionais/e2e no topo (simulando fluxos completos do usuário).

2. Estruturação da suíte de testes

Uma suíte bem organizada começa com uma estrutura de diretórios clara. Adote convenções como:

/projeto
  /src
  /tests
    /unit
    /integration
    /e2e
    /fixtures
    /mocks

Naming conventions como *.test.js ou *.spec.js facilitam a descoberta automática pelos runners. A escolha do framework depende do contexto: Jest é excelente para projetos React e oferece cobertura integrada; Mocha é flexível e agnóstico; Cypress e Playwright dominam o cenário e2e.

A configuração inicial envolve instalar dependências e definir scripts no package.json:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom
npm install --save-dev playwright @playwright/test
npm install --save-dev supertest axios-mock-adapter

Scripts de execução:

"scripts": {
  "test:unit": "jest --coverage",
  "test:integration": "jest --config jest.integration.config.js",
  "test:e2e": "playwright test",
  "test:all": "npm run test:unit && npm run test:integration && npm run test:e2e"
}

3. Testes unitários para componentes e funções

Testes unitários isolam a menor unidade de código — funções puras, hooks ou componentes renderizados. Mocks e stubs substituem dependências externas (API, módulos). O objetivo é testar lógica de negócio e estados de UI sem efeitos colaterais.

Exemplo prático: validação de formulário e cálculo de frete.

// utils/frete.js
export function calcularFrete(cep, peso) {
  if (!cep || cep.length !== 8) throw new Error('CEP inválido');
  if (peso <= 0) throw new Error('Peso deve ser positivo');
  const taxaBase = 10;
  const taxaPorKg = 2.5;
  return taxaBase + (peso * taxaPorKg);
}

// __tests__/frete.test.js
import { calcularFrete } from '../utils/frete';

describe('calcularFrete', () => {
  test('deve calcular frete para CEP válido e peso positivo', () => {
    expect(calcularFrete('12345678', 5)).toBe(22.5);
  });

  test('deve lançar erro para CEP inválido', () => {
    expect(() => calcularFrete('123', 5)).toThrow('CEP inválido');
  });

  test('deve lançar erro para peso zero', () => {
    expect(() => calcularFrete('12345678', 0)).toThrow('Peso deve ser positivo');
  });
});

Para componentes React, use Testing Library para testar estados:

// __tests__/Formulario.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Formulario from '../components/Formulario';

test('exibe mensagem de erro quando campo obrigatório está vazio', () => {
  render(<Formulario />);
  fireEvent.click(screen.getByText('Enviar'));
  expect(screen.getByText('Nome é obrigatório')).toBeInTheDocument();
});

4. Testes de integração com API e banco de dados

Testes de integração validam a comunicação entre módulos: requisições HTTP, autenticação e persistência. Use supertest para simular chamadas ao servidor Express ou axios-mock-adapter para interceptar chamadas no frontend.

// integration/api.test.js
const request = require('supertest');
const app = require('../src/app');

describe('POST /api/login', () => {
  test('deve retornar token para credenciais válidas', async () => {
    const response = await request(app)
      .post('/api/login')
      .send({ email: 'user@test.com', password: '123456' });
    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('token');
  });

  test('deve retornar 401 para credenciais inválidas', async () => {
    const response = await request(app)
      .post('/api/login')
      .send({ email: 'invalido@test.com', password: 'wrong' });
    expect(response.status).toBe(401);
  });
});

Para banco de dados, configure um ambiente isolado:

// integration/db.test.js
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database(':memory:');

beforeAll(() => {
  db.run('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
  db.run('INSERT INTO users (name) VALUES ("Alice")');
});

test('deve retornar usuário por ID', (done) => {
  db.get('SELECT name FROM users WHERE id = 1', (err, row) => {
    expect(row.name).toBe('Alice');
    done();
  });
});

afterAll(() => {
  db.close();
});

5. Testes end-to-end (e2e) com ferramentas modernas

Testes e2e simulam o comportamento real do usuário no navegador. Playwright e Cypress são as ferramentas mais robustas, oferecendo suporte a múltiplos navegadores, screenshots e vídeos.

Exemplo com Playwright:

// e2e/login.spec.js
const { test, expect } = require('@playwright/test');

test('usuário consegue fazer login com sucesso', async ({ page }) => {
  await page.goto('https://meuapp.com/login');
  await page.fill('[data-testid="email"]', 'user@test.com');
  await page.fill('[data-testid="password"]', '123456');
  await page.click('[data-testid="submit"]');
  await expect(page).toHaveURL(/dashboard/);
  await expect(page.locator('[data-testid="welcome"]')).toContainText('Bem-vindo');
});

test('exibe erro para senha incorreta', async ({ page }) => {
  await page.goto('https://meuapp.com/login');
  await page.fill('[data-testid="email"]', 'user@test.com');
  await page.fill('[data-testid="password"]', 'wrong');
  await page.click('[data-testid="submit"]');
  await expect(page.locator('[data-testid="error"]')).toBeVisible();
});

Para lidar com elementos dinâmicos e timeouts, use estratégias como waitForSelector e autoWaiting:

await page.waitForSelector('[data-testid="loading-spinner"]', { state: 'hidden' });
await page.waitForTimeout(2000); // evitar race conditions

6. Integração contínua e pipeline de testes

Integrar testes ao pipeline CI/CD garante que falhas sejam detectadas antes do deploy. Exemplo de configuração para GitHub Actions:

# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]

jobs:
  test:
    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
      - run: npm run test:integration
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-screenshots
          path: test-results/

Para paralelização, configure o Playwright para rodar em múltiplos workers:

// playwright.config.js
module.exports = {
  workers: process.env.CI ? 4 : 1,
  fullyParallel: true,
};

Relatórios de cobertura com Istanbul:

npm test -- --coverage --collectCoverageFrom='src/**/*.js'

7. Manutenção e boas práticas em testes automatizados

Testes frágeis são o maior inimigo da automação. Para evitá-los:

  • Use data-testid em vez de classes CSS ou seletores aninhados
  • Evite depender de timing exatos; prefira waitFor e toBeVisible
  • Isolar testes (cada teste deve ser independente)

Estratégias de refatoração:

// Ruim: testa implementação
expect(component.state().isLoading).toBe(true);

// Bom: testa comportamento
expect(screen.getByText('Carregando...')).toBeInTheDocument();

Métricas de qualidade recomendadas:

  • Cobertura mínima: 80% para unitários, 60% para integração
  • Tempo de execução: suíte completa < 10 minutos
  • Falsos positivos: menos de 1% das execuções

Manter testes junto com o código (TDD ou BDD) reduz dívida técnica e garante que a suíte evolua organicamente com o projeto.


Referências