Free

Jalankan tes otomatis dengan Hooks: Claude langsung tahu jika kode bermasalah

Gunakan PostToolUse Hooks pada alat Write/Edit untuk menjalankan tes setiap kali file diubah — kegagalan langsung dikembalikan ke Claude untuk diperbaiki.


Claude Code mengedit kode dengan cepat — tapi tidak menjalankan tes sendiri. Secara default, begitu selesai menulis file, tugasnya dianggap selesai. Tanpa instruksi eksplisit, Claude tidak tahu apakah perubahan tersebut merusak sesuatu.

Hook PostToolUse menyelesaikan masalah ini: setiap kali Claude menulis file, tes berjalan otomatis. Jika gagal, hasilnya langsung dikirim kembali ke Claude untuk segera diperbaiki.


Cara kerjanya

Claude Code memodifikasi file menggunakan dua alat: Write (membuat atau menimpa) dan Edit (perubahan sebagian). Memasang Hook di fase PostToolUse alat-alat ini memicu eksekusi tes setiap kali file disimpan ke disk.

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

Hook menerima JSON lengkap dari pemanggilan alat melalui stdin, termasuk path file yang dimodifikasi. Menggunakan path tersebut untuk memutuskan tes mana yang dijalankan adalah kuncinya.

Aturan kode keluar (PostToolUse):
- exit 0: tes lulus — Claude melanjutkan tanpa gangguan
- exit 2: tes gagal — isi stderr disuntikkan kembali ke Claude, yang melihat error dan mencoba memperbaikinya


Versi dasar: pilih perintah tes berdasarkan tipe file

Pendekatan paling sederhana: periksa ekstensi file dan jalankan suite tes yang sesuai.

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

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

# Jangan picu tes saat Claude mengedit file tes itu sendiri
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 "Tes gagal (file dimodifikasi: $file):" >&2
  echo "$result" >&2
  exit 2
fi

Versi lanjutan: targetkan file tes yang spesifik

Menjalankan seluruh suite setiap kali perubahan terlalu lambat. Gunakan path file yang dimodifikasi untuk menemukan hanya tes yang relevan.

Dalam proyek Python, src/auth/login.py biasanya dipetakan ke 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 "Tes gagal:" >&2
  echo "$result" >&2
  exit 2
fi

Untuk JavaScript/TypeScript, Jest mendukung pencocokan berdasarkan pola nama file:

#!/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 "Tes gagal:" >&2
  echo "$result" >&2
  exit 2
fi

Konfigurasi lengkap

Mencakup Write dan Edit, dengan perlindungan timeout agar tes yang macet tidak memblokir 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

# Lewati file tes itu sendiri dan file non-kode
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 "⚠ Tes gagal (file: $(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"
          }
        ]
      }
    ]
  }
}

Beberapa hal penting

Jangan picu tes saat mengedit file tes

Saat Claude mengedit file tes, tes itu sendiri belum selesai. Menjalankannya saat itu terlalu dini. Baris filter ini penting:

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

Write dan Edit menggunakan nama field berbeda di tool_input

Write menggunakan file_path, Edit menggunakan path. Tangani keduanya dengan fallback:

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

Kendalikan timeout tes

Suite yang lambat dikombinasikan dengan pengeditan yang sering dapat membekukan sesi. timeout 60 adalah default yang wajar; sesuaikan dengan durasi tes proyek yang sebenarnya.

Level proyek, bukan global

Hook ini sebaiknya ada di konfigurasi level proyek (.claude/settings.json). Perintah tes terlalu bervariasi antar proyek. Untuk versi global, skrip perlu mendeteksi tipe proyek secara dinamis.


Hasilnya

Dengan konfigurasi ini, ritme kerja Claude menjadi:

  1. Mengedit file sumber
  2. Hook otomatis menjalankan tes yang relevan
  3. Tes gagal → Claude melihat output error dan terus mengedit sampai lulus
  4. Tes lulus → diam, Claude melanjutkan ke langkah berikutnya

Tidak perlu terus berkata "jalankan tesnya". Setiap perubahan Claude memiliki jaring pengaman tes.