Impact analysis: rodando apenas testes afetados por um PR

1. O Problema: Testes Lentos e Ineficiência no CI

Em projetos de software que crescem ao longo dos anos, a suíte de testes tende a se expandir proporcionalmente ao código-fonte. Um cenário típico em grandes repositórios: a cada push em um Pull Request (PR), dezenas de milhares de testes são executados — incluindo aqueles que nada têm a ver com as alterações propostas. O resultado é um ciclo de feedback lento, consumo excessivo de recursos computacionais e desenvolvedores esperando minutos (ou horas) por uma validação que poderia ser concluída em segundos.

O custo computacional não é trivial. Em empresas com múltiplos pipelines paralelos, rodar a suíte completa a cada commit pode significar milhares de horas de CPU desperdiçadas por mês. Além disso, o tempo de espera desestimula práticas saudáveis como commits frequentes e revisões iterativas. Para projetos monolíticos ou microsserviços interdependentes, essa ineficiência se torna um gargalo crítico no fluxo de desenvolvimento.

2. Fundamentos: Como o Git Permite Identificar Mudanças

O Git oferece ferramentas nativas para detectar exatamente quais arquivos foram alterados entre dois pontos do histórico. A base de qualquer estratégia de análise de impacto é o comando git diff.

# Comparar a branch atual (HEAD) com a branch base (main)
git diff main...HEAD --name-only

Esse comando lista todos os arquivos modificados, adicionados ou deletados no PR, excluindo aqueles que já existem em main. Outra abordagem útil é usar git log para listar arquivos alterados em um intervalo de commits:

# Listar arquivos modificados nos últimos 3 commits
git log --name-only --oneline -3

O mapeamento desses arquivos para módulos ou diretórios específicos é o próximo passo. Se o projeto segue uma estrutura padronizada (ex.: src/module_a/, tests/module_a/), podemos filtrar por diretório:

git diff main...HEAD --name-only | grep '^src/' | sed 's|src/||' | cut -d'/' -f1 | sort -u

Esse comando extrai os nomes dos módulos alterados dentro de src/, que podem então ser usados para selecionar os testes correspondentes.

3. Estratégia de Mapeamento: Arquivo → Teste

Existem três abordagens principais para mapear arquivos de código a seus respectivos testes:

Abordagem manual: baseada em convenções de nomenclatura ou arquivos de configuração. Por exemplo, se todo arquivo src/user.service.ts tem um teste correspondente em tests/user.service.test.ts, a correspondência é direta. Em projetos maiores, um arquivo impact-map.json pode definir explicitamente quais testes são afetados por cada diretório.

Abordagem automatizada: utiliza análise estática de dependências. Ferramentas como dependency-cruiser (JavaScript) ou pydeps (Python) podem rastrear imports e exports, construindo um grafo de dependências. A partir dos arquivos alterados, navega-se pelo grafo para encontrar todos os testes que dependem direta ou indiretamente daquelas mudanças.

Ferramentas existentes: alguns runners de teste já oferecem suporte nativo:
- Jest: jest --onlyChanged (executa apenas testes relacionados a arquivos modificados)
- Pytest: pytest --last-failed (útil para reexecução, mas não para impacto direto)
- Scripts customizados combinando git diff com o runner de teste escolhido

4. Implementação Prática com Git e Scripts

Abaixo, um script shell funcional que extrai arquivos modificados, filtra por diretório relevante e gera uma lista de testes a executar:

#!/bin/bash
# Script: run-impacted-tests.sh
# Uso: ./run-impacted-tests.sh <branch-base>

BASE_BRANCH=${1:-main}

# 1. Obter arquivos modificados no PR
CHANGED_FILES=$(git diff "$BASE_BRANCH"...HEAD --name-only)

# 2. Filtrar apenas arquivos de código-fonte (ex.: .py, .ts)
SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(py|ts|js)$')

# 3. Extrair módulos únicos (ex.: "user", "payment")
MODULES=$(echo "$SOURCE_FILES" | sed 's|^src/||' | cut -d'/' -f1 | sort -u)

# 4. Mapear módulos para diretórios de teste
TEST_DIRS=""
for MODULE in $MODULES; do
  if [ -d "tests/$MODULE" ]; then
    TEST_DIRS="$TEST_DIRS tests/$MODULE"
  fi
done

# 5. Executar testes apenas nos diretórios mapeados
if [ -n "$TEST_DIRS" ]; then
  echo "Executando testes nos diretórios: $TEST_DIRS"
  pytest $TEST_DIRS -v
else
  echo "Nenhum teste impactado encontrado. Executando suíte completa como fallback."
  pytest -v
fi

Para ambientes JavaScript/TypeScript com Jest, o mapeamento pode ser ainda mais simples:

# Usando Jest com --onlyChanged (requer configuração de dependências)
git diff main...HEAD --name-only | xargs npx jest --onlyChanged --findRelatedTests

5. Integração com CI e Estratégias de Fallback

Em pipelines de CI (GitHub Actions, GitLab CI, Jenkins), a análise de impacto deve ser executada antes da suíte de testes completa. Um exemplo para GitHub Actions:

name: CI Impact Analysis

on:
  pull_request:
    branches: [main]

jobs:
  test-impacted:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # necessário para git diff com branch base

      - name: Run impacted tests
        run: |
          chmod +x run-impacted-tests.sh
          ./run-impacted-tests.sh origin/main

Casos especiais que devem disparar a suíte completa:
- Alterações em package.json, requirements.txt, ou Dockerfile
- Modificações em arquivos de pipeline (.github/workflows/, .gitlab-ci.yml)
- Mudanças em diretórios de configuração global (config/, infra/)

Fallback seguro: se o script de análise falhar (ex.: branch base não encontrada, erro de parsing), execute todos os testes. Isso evita falsos negativos que poderiam passar despercebidos.

6. Limitações e Cuidados com a Abordagem

A análise de impacto baseada apenas em git diff tem limitações importantes:

Falsos negativos: uma alteração em src/utils/helpers.py pode quebrar testes em tests/payment/test_checkout.py se houver dependência indireta. O mapeamento por diretório não captura esses casos.

Dependências transitivas: testes de integração que dependem de múltiplos módulos podem ser afetados por mudanças em qualquer parte da cadeia. Uma análise estática completa é necessária para cobrir esses cenários.

Manutenção do mapeamento: quando arquivos são renomeados ou refatorados, o mapeamento manual precisa ser atualizado. Ferramentas automatizadas reduzem esse problema, mas exigem configuração inicial e podem ter falsos positivos.

7. Casos de Uso Avançados e Ferramentas Especializadas

Para projetos que exigem precisão maior, ferramentas especializadas oferecem análise de impacto em nível de grafo de dependências:

  • Nx (monorepos): com o comando nx affected:test, calcula exatamente quais projetos e testes são impactados por um conjunto de alterações, usando análise de dependências entre pacotes.
  • Bazel: constrói e testa apenas os targets afetados, com cache distribuído e granularidade fina.
  • Knapsack Pro: inteligente para paralelizar testes em CI, mas também oferece detecção de testes impactados.

O git bisect pode ser combinado com análise de impacto para depuração: ao identificar um commit que introduziu uma falha, a análise de impacto mostra quais testes deveriam ter falhado naquele ponto, ajudando a isolar a causa raiz.

Estratégias híbridas são recomendadas para projetos críticos: execute sempre os testes impactados identificados pela análise, mas adicione uma amostra aleatória (ex.: 10%) de testes não impactados para detectar efeitos colaterais inesperados.


Referências