Estratégias de testes para código legado sem cobertura existente

1. Diagnóstico Inicial e Mapeamento do Código Legado

Antes de escrever qualquer teste, é essencial realizar um diagnóstico completo do código legado. O primeiro passo é identificar as áreas críticas de negócio e alto risco de falha. Utilize ferramentas de análise estática como SonarQube ou ESLint para detectar dependências ocultas e alto acoplamento.

Exemplo de inventário de funcionalidades sem cobertura:

Funcionalidade: Cálculo de frete
Arquivo: src/Shipping/Calculator.php
Linhas: 1-450
Dependências: Database, API de Correios, Cache
Risco: Alto (impacto financeiro direto)
Cobertura atual: 0%

Crie uma matriz de priorização baseada em:
- Frequência de uso da funcionalidade
- Impacto de falha no negócio
- Complexidade do código
- Número de dependências externas

2. Estratégias de Caracterização (Characterization Tests)

Os testes de caracterização capturam o comportamento atual do sistema sem exigir conhecimento da especificação original. Eles documentam o que o código faz hoje, não o que deveria fazer.

Exemplo de teste caracterizador para uma função legada:

// Função legada sem testes
function calcularDesconto(valor, categoria) {
    if (categoria === 'VIP') {
        return valor * 0.8;
    } else if (categoria === 'Regular') {
        return valor * 0.9;
    } else {
        return valor;
    }
}

// Teste caracterizador
describe('calcularDesconto', () => {
    it('deve retornar o comportamento atual para categoria VIP', () => {
        const resultado = calcularDesconto(100, 'VIP');
        expect(resultado).toBe(80); // Comportamento atual documentado
    });

    it('deve retornar o comportamento atual para categoria Regular', () => {
        const resultado = calcularDesconto(100, 'Regular');
        expect(resultado).toBe(90);
    });

    it('deve retornar o comportamento atual para categoria desconhecida', () => {
        const resultado = calcularDesconto(100, 'Gold');
        expect(resultado).toBe(100);
    });
});

Use snapshots para capturar saídas complexas e asserts genéricos para documentar efeitos colaterais.

3. Introdução de Testes de Integração com Isolamento Controlado

Antes de testar unidades individuais, crie testes de integração focados em fluxos críticos. Use mocking seletivo para substituir dependências externas como bancos de dados, APIs e sistemas de arquivos.

Exemplo de teste de integração com mocking controlado:

describe('Processamento de Pedido', () => {
    it('deve processar pedido completo sem chamar API externa', () => {
        // Mock seletivo apenas para a API de pagamento
        const apiMock = jest.spyOn(paymentAPI, 'processar');
        apiMock.mockResolvedValue({ status: 'aprovado' });

        const pedido = {
            id: 123,
            itens: [{ produto: 'A', quantidade: 2 }],
            total: 200
        };

        const resultado = processarPedido(pedido);

        expect(resultado.status).toBe('concluído');
        expect(apiMock).toHaveBeenCalledTimes(1);
    });
});

Para funções longas e acopladas, teste os caminhos principais sem refatoração prévia:

describe('FunçãoLegadaLonga', () => {
    it('deve executar caminho feliz sem lançar exceção', () => {
        const entrada = { /* dados mínimos necessários */ };
        expect(() => funcaoLonga(entrada)).not.toThrow();
    });

    it('deve retornar valor esperado para caso típico', () => {
        const entrada = { valor: 100, tipo: 'padrao' };
        const resultado = funcaoLonga(entrada);
        expect(resultado).toBeDefined();
        expect(typeof resultado).toBe('number');
    });
});

4. Refatoração Guiada por Testes (Test-Driven Refactoring)

Identifique seams (pontos de injeção) no código para inserir testes gradualmente. Use wrappers para separar lógica pura de efeitos colaterais.

Exemplo de identificação de seam e refatoração gradual:

// Código original sem seams
function processarRelatorio(data) {
    const dados = database.buscar(data.id);
    const calculado = calcularMetricas(dados);
    arquivo.escrever('relatorio.txt', calculado);
    return calculado;
}

// Passo 1: Identificar seam na dependência de banco
function processarRelatorio(data, databaseOverride) {
    const db = databaseOverride || database;
    const dados = db.buscar(data.id);
    const calculado = calcularMetricas(dados);
    arquivo.escrever('relatorio.txt', calculado);
    return calculado;
}

// Passo 2: Teste caracterizador com seam
describe('processarRelatorio', () => {
    it('deve calcular métricas corretamente com dados mockados', () => {
        const mockDB = { buscar: jest.fn().mockReturnValue([1, 2, 3]) };
        const resultado = processarRelatorio({ id: 1 }, mockDB);
        expect(resultado).toEqual(/* valor esperado */);
    });
});

Ciclo de refatoração guiada por testes:
1. Escreva teste caracterizador para comportamento atual
2. Refatore pequena parte do código
3. Execute testes para verificar comportamento inalterado
4. Repita

5. Criação de uma Rede de Segurança com Testes de Regressão

Priorize testes para funcionalidades com maior frequência de bugs. Automatize a execução em pipeline CI/CD com execução incremental.

Exemplo de configuração de pipeline com testes incrementais:

# .github/workflows/test-legado.yml
name: Testes de Regressão
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Executar testes modificados
        run: |
          # Identifica arquivos alterados
          git diff --name-only HEAD~1 > changed_files.txt
          # Executa apenas testes relacionados
          npx jest --listTests --findRelatedTags $(cat changed_files.txt)

Use cobertura diferencial para monitorar progresso:

Cobertura antes: 0%
Cobertura após primeiro sprint: 15%
Cobertura após segundo sprint: 35%
Meta: 40% para código legado

6. Lidando com Código Não Testável: Técnicas Específicas

Para código altamente acoplado, use injeção manual e interfaces adaptadoras:

// Código não testável original
function salvarUsuario(nome) {
    const conn = new DatabaseConnection('localhost');
    conn.execute(`INSERT INTO usuarios (nome) VALUES ('${nome}')`);
    conn.close();
}

// Com adaptador para teste
function salvarUsuario(nome, dbAdapter) {
    const db = dbAdapter || new DatabaseConnection('localhost');
    db.execute(`INSERT INTO usuarios (nome) VALUES ('${nome}')`);
    db.close();
}

// Teste com spy
describe('salvarUsuario', () => {
    it('deve executar INSERT no banco', () => {
        const spy = { execute: jest.fn(), close: jest.fn() };
        salvarUsuario('João', spy);
        expect(spy.execute).toHaveBeenCalledWith(
            expect.stringContaining('INSERT INTO usuarios')
        );
    });
});

Para entradas imprevisíveis, use property-based testing:

describe('calcularFrete', () => {
    it('deve sempre retornar valor positivo para qualquer peso', () => {
        for (let peso = 0; peso < 1000; peso += 0.5) {
            const resultado = calcularFrete(peso);
            expect(resultado).toBeGreaterThanOrEqual(0);
        }
    });

    it('deve ser monotônico crescente com o peso', () => {
        let anterior = 0;
        for (let peso = 0; peso < 100; peso++) {
            const atual = calcularFrete(peso);
            expect(atual).toBeGreaterThanOrEqual(anterior);
            anterior = atual;
        }
    });
});

7. Manutenção Sustentável e Cultura de Testes no Time

Defina métricas realistas de cobertura para código legado:

Métricas do Projeto Legado:
- Cobertura mínima obrigatória: 40%
- Cobertura para código novo: 80%
- Meta trimestral: +10% de cobertura
- Exceções documentadas: 3 módulos (aprovadas em revisão)

Documente decisões de teste e razões para não testar certas partes:

Módulo: legacy/payment/v1.js
Decisão: Não testado
Motivo: Será substituído no próximo release
Data: 2024-01-15
Responsável: Maria Silva
Revisão agendada: 2024-03-01

Realize revisões periódicas do conjunto de testes para eliminar redundâncias e falsos positivos.

Referências