Free

Executar testes automaticamente com Hooks: Claude sabe na hora se algo quebrou

Use PostToolUse Hooks nas ferramentas Write/Edit para rodar testes após cada alteração de arquivo — as falhas são retornadas ao Claude para correção imediata.


Claude Code edita código rápido, mas não executa testes por conta própria. Por padrão, assim que escreve um arquivo considera o trabalho concluído — a menos que você peça explicitamente, ele não sabe se a mudança quebrou alguma coisa.

Um Hook PostToolUse resolve isso: toda vez que Claude escreve um arquivo, os testes rodam automaticamente. Se falharem, o resultado é devolvido diretamente ao Claude para correção imediata.


Como funciona

Claude Code modifica arquivos usando duas ferramentas: Write (criar ou sobrescrever) e Edit (alterações parciais). Adicionar um Hook na fase PostToolUse dessas ferramentas dispara uma execução de testes sempre que um arquivo é salvo em disco.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/run-tests.sh"
          }
        ]
      }
    ]
  }
}

O Hook recebe o JSON completo da chamada de ferramenta via stdin, incluindo o caminho do arquivo modificado. Usar esse caminho para decidir quais testes executar é a chave dessa abordagem.

Regras de código de saída (PostToolUse):
- exit 0: testes passaram — Claude continua sem interrupção
- exit 2: testes falharam — o conteúdo do stderr é injetado de volta ao Claude, que vê o erro e tenta corrigir


Versão básica: escolher comando pelo tipo de arquivo

Abordagem mais simples: verificar a extensão do arquivo e rodar o conjunto de testes correspondente.

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

[[ -z "$file" ]] && exit 0

# Não disparar testes quando Claude edita o próprio arquivo de teste
echo "$file" | grep -qE '(test|spec)\.' && exit 0

case "$file" in
  *.py)
    cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
    result=$(python -m pytest --tb=short -q 2>&1)
    ;;
  *.ts|*.js|*.tsx|*.jsx)
    cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
    result=$(npm test -- --passWithNoTests 2>&1)
    ;;
  *.go)
    dir=$(dirname "$file")
    result=$(go test "./$dir/..." 2>&1)
    ;;
  *.rb)
    cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
    result=$(bundle exec rspec --format progress 2>&1)
    ;;
  *)
    exit 0
    ;;
esac

exit_code=$?

if [[ $exit_code -ne 0 ]]; then
  echo "Testes falharam (arquivo modificado: $file):" >&2
  echo "$result" >&2
  exit 2
fi

Versão avançada: apontar para o arquivo de teste específico

Rodar a suite completa a cada mudança é lento. Use o caminho do arquivo modificado para localizar apenas os testes relevantes.

Em um projeto Python, src/auth/login.py costuma mapear para tests/auth/test_login.py:

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

[[ -z "$file" || "$file" != *.py ]] && exit 0
echo "$file" | grep -qE '(test_|_test\.py)' && exit 0

root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root"

basename=$(basename "$file" .py)
test_file=$(find tests -name "test_${basename}.py" 2>/dev/null | head -1)

if [[ -n "$test_file" ]]; then
  result=$(python -m pytest "$test_file" --tb=short -q 2>&1)
else
  result=$(python -m pytest --tb=short -q 2>&1)
fi

if [[ $? -ne 0 ]]; then
  echo "Testes falharam:" >&2
  echo "$result" >&2
  exit 2
fi

Para JavaScript/TypeScript, o Jest suporta filtragem por padrão de nome de arquivo:

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

[[ -z "$file" ]] && exit 0
echo "$file" | grep -qE '\.(test|spec)\.' && exit 0
echo "$file" | grep -qE '\.(ts|tsx|js|jsx)$' || exit 0

root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root"

basename=$(basename "$file" | sed 's/\.[^.]*$//')
result=$(npx jest --testPathPattern="$basename" --passWithNoTests 2>&1)

if [[ $? -ne 0 ]]; then
  echo "Testes falharam:" >&2
  echo "$result" >&2
  exit 2
fi

Configuração completa

Cobrir tanto Write quanto Edit, com proteção de timeout para evitar que testes travados bloqueiem o Claude:

.claude/hooks/run-tests.sh:

#!/bin/bash
set -euo pipefail

input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

[[ -z "$file" ]] && exit 0

# Pular arquivos de teste e arquivos que não são código
echo "$file" | grep -qE '(test_|_test\.|\.test\.|\.spec\.)' && exit 0
echo "$file" | grep -qE '\.(py|ts|js|tsx|jsx|go|rb)$' || exit 0

root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root"

run_test() {
  timeout 60 "$@" 2>&1
}

case "$file" in
  *.py)
    result=$(run_test python -m pytest --tb=short -q)
    ;;
  *.ts|*.tsx|*.js|*.jsx)
    basename=$(basename "$file" | sed 's/\.[^.]*$//')
    result=$(run_test npx jest --testPathPattern="$basename" --passWithNoTests)
    ;;
  *.go)
    dir=$(dirname "$file")
    result=$(run_test go test "./$dir/...")
    ;;
  *.rb)
    result=$(run_test bundle exec rspec --format progress)
    ;;
  *)
    exit 0
    ;;
esac

if [[ $? -ne 0 ]]; then
  echo "⚠ Testes falharam (arquivo: $(basename "$file"))" >&2
  echo "" >&2
  echo "$result" >&2
  exit 2
fi

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/run-tests.sh"
          }
        ]
      }
    ]
  }
}

Pontos importantes

Não disparar testes ao editar arquivos de teste

Quando Claude edita um arquivo de teste, o próprio teste ainda não está completo. Executá-lo nesse momento é prematuro. Essa linha de filtro é essencial:

echo "$file" | grep -qE '(test_|_test\.|\.test\.|\.spec\.)' && exit 0

Write e Edit usam nomes de campo diferentes em tool_input

Write usa file_path, Edit usa path. Tratar os dois com um fallback:

file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

Controlar o timeout dos testes

Suites lentas combinadas com edições frequentes podem travar a sessão. timeout 60 é um padrão razoável; ajuste conforme a duração real dos testes do projeto.

Nível de projeto, não global

Este Hook pertence à configuração de nível de projeto (.claude/settings.json). Os comandos de teste variam demais entre projetos. Para uma versão global, o script precisa detectar o tipo de projeto dinamicamente.


O resultado

Com essa configuração, o ritmo de trabalho do Claude fica assim:

  1. Edita um arquivo fonte
  2. Hook executa automaticamente os testes relevantes
  3. Testes falharam → Claude vê a saída de erro e continua editando até passar
  4. Testes passaram → silêncio, Claude segue para o próximo passo

Sem precisar ficar dizendo "roda os testes". Cada mudança do Claude tem uma rede de segurança de testes.