Free

Hooks ile testleri otomatik çalıştırın: Claude kod değişikliklerinin hatalı olup olmadığını anında bilir

Write/Edit araçlarına PostToolUse Hooks ekleyerek her dosya değişikliğinde testleri otomatik çalıştırın — hatalar Claude'a geri gönderilerek anında düzeltilir.


Claude Code kodu hızlı düzenler — ama testleri kendi başına çalıştırmaz. Varsayılan olarak, bir dosya yazdıktan sonra görevi tamamlanmış sayar. Açıkça söylemediğiniz sürece, değişikliğin bir şeyi bozup bozmadığını bilmez.

Bir PostToolUse Hook bu sorunu çözer: Claude her dosya yazdığında testler otomatik çalışır. Başarısız olurlarsa sonuç doğrudan Claude'a iletilir ve hemen düzeltmeye girişir.


Nasıl çalışır

Claude Code dosyaları iki araçla değiştirir: Write (oluştur veya üzerine yaz) ve Edit (kısmi değişiklikler). Bu araçların PostToolUse aşamasına bir Hook eklemek, her dosya kaydedildiğinde testlerin otomatik çalışmasını sağlar.

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

Hook, stdin üzerinden araç çağrısının tam JSON'ını alır; değiştirilen dosyanın yolu da içindedir. Hangi testlerin çalıştırılacağına karar vermek için bu yolu kullanmak yaklaşımın özüdür.

Çıkış kodu kuralları (PostToolUse):
- exit 0: testler geçti — Claude kesintisiz devam eder
- exit 2: testler başarısız — stderr içeriği Claude'a enjekte edilir, hata görülür ve düzeltilmeye çalışılır


Temel versiyon: dosya türüne göre test komutu seçimi

En basit yaklaşım: dosya uzantısına bakıp karşılık gelen test paketini çalıştırmak.

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

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

# Claude bir test dosyasını düzenlerken test tetikleme
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 "Testler başarısız (değiştirilen dosya: $file):" >&2
  echo "$result" >&2
  exit 2
fi

Gelişmiş versiyon: belirli test dosyasını hedefleme

Her değişiklikte tüm paketi çalıştırmak yavaş. Değiştirilen dosyanın yolunu kullanarak yalnızca ilgili testleri bulun.

Python projesinde src/auth/login.py genellikle tests/auth/test_login.py'e karşılık gelir:

#!/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 "Testler başarısız:" >&2
  echo "$result" >&2
  exit 2
fi

JavaScript/TypeScript için Jest, dosya adı deseniyle filtrelemeyi destekler:

#!/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 "Testler başarısız:" >&2
  echo "$result" >&2
  exit 2
fi

Tam yapılandırma

Write ve Edit'i kapsayın, askıda kalan testlerin Claude'u bloke etmemesi için timeout koruması ekleyin:

.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

# Test dosyalarını ve kod dışı dosyaları atla
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 "⚠ Testler başarısız (tetikleyen dosya: $(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"
          }
        ]
      }
    ]
  }
}

Dikkat edilmesi gerekenler

Test dosyalarını düzenlerken test tetiklememe

Claude bir test dosyasını düzenlerken, test henüz tamamlanmamış. Bu noktada çalıştırmak erken. Bu filtre satırı kritik:

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

Write ve Edit, tool_input'ta farklı alan adları kullanır

Write file_path, Edit path kullanır. Fallback ile her ikisini de ele alın:

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

Test zaman aşımlarını kontrol altında tutun

Yavaş test paketleri sık düzenlemelerle birleşince oturumu dondurabilir. timeout 60 makul bir varsayılan; projenin gerçek test süresine göre ayarlayın.

Proje düzeyi, global değil

Bu Hook proje düzeyi yapılandırmaya (.claude/settings.json) aittir. Test komutları projeler arasında çok farklı. Global bir versiyon için, betik proje türünü dinamik olarak algılamalı.


Sonuç

Bu yapılandırmayla Claude'un çalışma ritmi şu hale gelir:

  1. Kaynak dosyayı düzenler
  2. Hook ilgili testleri otomatik çalıştırır
  3. Testler başarısız → Claude hata çıktısını görür ve geçene kadar düzenlemeye devam eder
  4. Testler geçti → sessizlik, Claude bir sonraki adıma geçer

Defalarca "testleri çalıştır" demeye gerek kalmaz. Claude'un her değişikliğinin bir test güvenlik ağı vardır.