点击蓝字
关注我们
上篇《Claude Code Hooks怎么用自动门禁让AI边写测试边自动修?》我们用 Claude Code Hooks 拦截硬编码等待和选择器污染,今天我们继续学习“缺少断言”的检查,并将三条规则合并为一个脚本,形成完整的 QA 门禁。
捕获缺少断言的问题
一个测试跑了一遍:导航、填表单、点按钮——然后啥也没断言——这是自动化测试套件里最危险的模式之一。
它会每次都通过,给你一种“覆盖率很好”的错觉,而等到真正出问题的时候,它什么也告诉不了你。Claude Code 也可能写出这样的测试,尤其是当它专注于“让测试跑通不报错”而不是“这个测试到底在验证什么”的时候。
下面这个 hook 会检查每个被编辑的测试文件,确保里面至少有一个断言:
# .claude/hooks/check_assertions.sh#!/bin/bashFILE=$1# 只检查测试文件if [[ ! "$FILE" == tests/*.py ]]; thenexit 0fi# 跳过不包含测试类或函数的文件if ! grep -qE "def test_" "$FILE"; thenexit 0fi# 统计文件中的测试函数数量TEST_COUNT=$(grep -cE "def test_" "$FILE")# 统计断言模式的数量ASSERTION_COUNT=$(grep -cE "assert |expect\(|should\.|to_be_|to_have_|to_contain_" "$FILE")if [ "$ASSERTION_COUNT" -eq 0 ]; thenecho ""echo "QA GATE FAILED: No assertions found in $FILE"echo "Every test must verify at least one expected outcome."echo "Examples:"echo " assert page.url.endswith('/dashboard')"echo " expect(page.locator(\"[data-testid='success-msg']\")).to_be_visible()"exit 1fi# 如果测试函数数量远多于断言数量,发出警告if [ "$TEST_COUNT" -gt "$ASSERTION_COUNT" ]; thenecho ""echo "QA GATE WARNING: $FILE has $TEST_COUNT test functions but only $ASSERTION_COUNT assertions."echo "Some tests may be missing meaningful verifications."fiexit0
对应的 hook 配置:
{"hooks": {"PostToolUse": [{"matcher": "Edit|Write","hooks": [{"type": "command","command": "bash .claude/hooks/check_assertions.sh $CLAUDE_TOOL_INPUT_FILE_PATH"}]}]}}
终端里看到的失败信息是这样的:
QA GATE FAILED: No assertions found in tests/e2e/test_profile.pyEvery test must verify at least one expected outcome.Examples:assert page.url.endswith('/dashboard')expect(page.locator("[data-testid='success-msg']")).to_be_visible()
警告的情况也同样有用。如果一个文件里有五个测试函数,却只有一个断言,这强烈暗示 Claude Code 写的测试只是跟 UI 做了交互,但并没有真正验证任何有意义的点。
在文件编辑阶段就捕获到这个问题,意味着你不需要等到生产环境部署失败才发现它。
把多个 Hooks 合并成一个完整的 QA 门禁
分别跑三个独立的 hook 也可以,但把它们合并到一个脚本里会更干净、更容易维护。一个脚本、一条 hook 配置、清晰的输出——一眼就能看出哪里失败了、为什么失败。
下面是合并后的脚本:
# .claude/hooks/qa_gate.sh#!/bin/bashFILE=$1VIOLATIONS=0# 只检查Python测试文件和页面对象if [[ ! "$FILE" == tests/*.py ]] && [[ ! "$FILE" == pages/*.py ]]; thenexit 0fiecho "Running QA gate checks on $FILE..."# 检查1:硬编码等待if grep -nE "time\.sleep\(|waitForTimeout\(|Thread\.sleep\(" "$FILE"; thenecho ""echo "FAILED [Hardcoded Wait]: Replace with an explicit wait condition."echo " Playwright: page.wait_for_selector(), expect(locator).to_be_visible()"echo " Selenium: WebDriverWait(driver, timeout).until(...)"VIOLATIONS=$((VIOLATIONS + 1))fi# 检查2:禁止的选择器if grep -nE "By\.XPATH|find_element\(By\.ID|find_element\(By\.CLASS_NAME|css=\'" "$FILE"; thenecho ""echo "FAILED [Forbidden Selector]: Use data-testid attributes only."echo " Correct: page.locator(\"[data-testid='submit-btn']\")"echo " Forbidden: By.XPATH, By.ID, By.CLASS_NAME, css selectors"VIOLATIONS=$((VIOLATIONS + 1))fi# 检查3:缺少断言(仅测试文件)if [[ "$FILE" == tests/*.py ]]; thenif grep -qE "def test_" "$FILE"; thenASSERTION_COUNT=$(grep -cE "assert |expect\(|should\.|to_be_|to_have_|to_contain_" "$FILE")TEST_COUNT=$(grep -cE "def test_" "$FILE")if [ "$ASSERTION_COUNT" -eq 0 ]; thenecho ""echo "FAILED [Missing Assertions]: Every test must verify at least one outcome."VIOLATIONS=$((VIOLATIONS + 1))elif [ "$TEST_COUNT" -gt "$ASSERTION_COUNT" ]; thenecho ""echo "WARNING [Assertion Coverage]: $TEST_COUNT tests but only $ASSERTION_COUNT assertions found."echo "Some tests may be missing meaningful verifications."fififi# 总结if [ $VIOLATIONS -gt 0 ]; thenecho ""echo "QA gate failed with $VIOLATIONS violation(s) in $FILE"echo "Claude Code will attempt to fix the issues above before continuing."exit 1fiecho "QA gate passed."exit 0
以及用来替代上面三个独立 hook 的单条配置:
{"hooks": {"PostToolUse": [{"matcher": "Edit|Write","hooks": [{"type": "command","command": "bash .claude/hooks/qa_gate.sh $CLAUDE_TOOL_INPUT_FILE_PATH"}]}]}}
当同一个文件里检测出多个违规时,输出会一次性清晰地告诉你所有需要修复的问题:
Running QA gate checks on tests/e2e/test_checkout.py...tests/e2e/test_checkout.py:14: time.sleep(2)FAILED [Hardcoded Wait]: Replace with an explicit wait condition.Playwright: page.wait_for_selector(), expect(locator).to_be_visible()Selenium: WebDriverWait(driver, timeout).until(...)tests/e2e/test_checkout.py:22: self.driver.find_element(By.ID, "confirm-btn").click()FAILED [Forbidden Selector]: Use data-testid attributes only.Correct: page.locator("[data-testid='confirm-btn']")Forbidden: By.XPATH, By.ID, By.CLASS_NAME, css selectorsQA gate failed with 2 violation(s) in tests/e2e/test_checkout.pyClaude Code will attempt to fix the issues above before continuing.
这里需要理解的一个重点是:接下来会发生什么。当 Claude Code 看到 hook 返回了非零的退出码时,它会读取输出、理解哪些东西被标记了、然后尝试修复这些违规,然后再继续。
在实际使用中,这意味着门禁几乎不会长时间阻断你的工作流。Claude Code 会自我修正、重新运行 hook、等一切通过之后再继续。你既得到了强制约束,又不会被频繁打断。
随着你的测试框架不断演进,qa_gate.sh 这个脚本会成为你想强制执行的所有新约定的自然落脚点。加一条检查,提交脚本,以后这个项目里的每一个 Claude Code 会话都会自动遵守它。
总结
糟糕的测试模式不总是因为粗心。它们之所以会出现,是因为反馈回路太慢了。
等到一个硬编码等待或一个缺失的断言被送到代码审查环节时,它已经浪费了某个人的时间。Claude Code hooks 把反馈时机提前到了最早可能的时刻——就在文件被写入的那一瞬间——而且不给你增加任何手动操作。
从 qa_gate.sh 脚本里的某一条检查开始,先熟悉 hooks 的行为方式,然后再逐步扩展。这套门禁会随着你的约定一起成长——你文档化的规则越多,这个门禁就越有用。
E n d

