Free

Tự động chạy test với Hooks: Claude biết ngay nếu code có lỗi sau khi sửa

Dùng PostToolUse Hooks trên công cụ Write/Edit để chạy test sau mỗi lần thay đổi file — kết quả thất bại được đẩy ngay về Claude để sửa.


Claude Code sửa code nhanh — nhưng không tự chạy test. Mặc định, sau khi viết xong file là coi như xong việc. Nếu không nói rõ, nó không biết thay đổi vừa rồi có phá vỡ gì không.

Hook PostToolUse giải quyết điều này: mỗi lần Claude viết file, test tự động chạy. Nếu thất bại, kết quả được đẩy thẳng về cho Claude để sửa ngay lập tức.


Cách hoạt động

Claude Code sửa đổi file bằng hai công cụ: Write (tạo mới hoặc ghi đè) và Edit (chỉnh sửa một phần). Gắn Hook vào phase PostToolUse của hai công cụ này sẽ kích hoạt chạy test mỗi khi file được lưu xuống đĩa.

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

Hook nhận toàn bộ JSON của lần gọi công cụ qua stdin, bao gồm đường dẫn file vừa sửa. Dùng đường dẫn đó để quyết định chạy test nào là mấu chốt.

Quy tắc exit code (PostToolUse):
- exit 0: test qua — Claude tiếp tục bình thường
- exit 2: test thất bại — nội dung stderr được đưa lại cho Claude, nó thấy lỗi và thử sửa


Phiên bản cơ bản: chọn lệnh test theo loại file

Đơn giản nhất: kiểm tra extension file rồi chạy test suite tương ứng.

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

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

# Không kích hoạt test khi Claude sửa chính file test
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 "Test thất bại (file đã sửa: $file):" >&2
  echo "$result" >&2
  exit 2
fi

Phiên bản nâng cao: nhắm đúng file test cụ thể

Chạy toàn bộ suite mỗi lần quá chậm. Dùng đường dẫn file vừa sửa để tìm đúng test liên quan.

Với project Python, src/auth/login.py thường tương ứng với 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 "Test thất bại:" >&2
  echo "$result" >&2
  exit 2
fi

Với JavaScript/TypeScript, Jest hỗ trợ lọc theo pattern tên 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 "Test thất bại:" >&2
  echo "$result" >&2
  exit 2
fi

Cấu hình đầy đủ

Bao phủ cả Write lẫn Edit, thêm timeout để tránh test bị treo làm kẹt 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

# Bỏ qua chính file test và file không phải code
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 "⚠ Test thất bại (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"
          }
        ]
      }
    ]
  }
}

Một số lưu ý

Không kích hoạt test khi sửa file test

Khi Claude đang sửa file test, bản thân test đó chưa hoàn chỉnh. Chạy nó lúc này là quá sớm. Dòng lọc này rất quan trọng:

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

Write và Edit dùng tên field khác nhau trong tool_input

Write dùng file_path, Edit dùng path. Xử lý cả hai bằng fallback:

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

Kiểm soát timeout test

Test suite chậm kết hợp với việc Claude sửa liên tục có thể làm đóng băng phiên làm việc. timeout 60 là mặc định hợp lý; điều chỉnh theo thời gian test thực tế của project.

Cấp project, không phải global

Hook này nên đặt trong cấu hình cấp project (.claude/settings.json). Lệnh test quá khác nhau giữa các project. Nếu muốn dùng bản global, script cần tự phát hiện loại project.


Kết quả

Với cấu hình này, nhịp làm việc của Claude trở thành:

  1. Sửa file source
  2. Hook tự động chạy test liên quan
  3. Test thất bại → Claude thấy output lỗi và tiếp tục sửa cho đến khi qua
  4. Test qua → im lặng, Claude tiếp tục bước tiếp theo

Không cần nhắc đi nhắc lại "chạy test đi". Mọi thay đổi của Claude đều có lưới an toàn từ test.