本文目录导读:

- 检查测试文件是否存在(覆盖率基本保障)
- 检测函数/方法长度(可测试性指标)
- 禁止硬编码外部依赖(不可测试模式)
- 检查全局状态修改
- 检查测试覆盖率是否达标(进阶方案)
- 检查 Mock 友好性(静态模式扫描)
- 组合脚本:完整的 pre-commit 可测试性检查
- 安装与使用
- 注意事项
Git 钩子无法直接测试代码逻辑,但可以通过静态分析和约定检查来阻止明显不可测试或难以测试的代码提交,以下是几种实用的检查策略及对应脚本示例。
检查测试文件是否存在(覆盖率基本保障)
在 pre-commit 钩子中,检查新修改的 .py 或 .js 文件是否对应有同名测试文件。
Python 示例(.git/hooks/pre-commit):
#!/bin/bash
echo "检查是否缺少测试文件..."
# 获取暂存区中新增/修改的 Python 文件
changed_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$' | grep -v '__init__' | grep -v 'test_')
for file in $changed_files; do
# 推断测试文件名:src/module.py -> tests/test_module.py
test_file=$(echo "$file" | sed 's|src/|tests/test_|')
if [ ! -f "$test_file" ]; then
echo "❌ 缺少测试文件:$test_file (对应源文件 $file)"
exit 1
fi
done
echo "✅ 测试文件检查通过"
检测函数/方法长度(可测试性指标)
超长函数通常难以单元测试,在 pre-commit 中扫描代码行数。
通用脚本:
#!/bin/bash
MAX_FUNCTION_LINES=50
# 扫描暂存区中的 Python 文件
git diff --cached --name-only | grep '\.py$' | while read file; do
# 使用 awk 粗略检测函数长度(按 def 关键字分割)
awk '/^def /{if(NR>1) print NR-1 ":" line}; {line=$0}' "$file" | \
while IFS=: read -r line_count func_name; do
if [ "$line_count" -gt "$MAX_FUNCTION_LINES" ]; then
echo "⚠️ 函数过长(${line_count}行): $func_name in $file"
echo " 建议拆分为多个小函数以便于测试"
exit 1
fi
done
done
禁止硬编码外部依赖(不可测试模式)
检测难以 Mock 的直接网络/数据库调用。
检测 requests.get() 等硬编码调用:
#!/bin/bash
# 检查是否直接在函数体内使用 requests 等(应通过参数注入)
git diff --cached -U0 | grep '^+' | grep -E 'requests\.(get|post|put)|open\(' | while read line; do
echo "❌ 发现硬编码外部调用: $line"
echo " 建议改为依赖注入方式以支持测试 Mock"
exit 1
done
检查全局状态修改
全局变量会使测试间相互污染,扫描新增的全局赋值。
#!/bin/bash
git diff --cached -U0 | grep '^+' | grep -E '^[a-z_]+ *= *' | \
while read line; do
# 过滤掉局部变量(缩进判断),这里简化判断
echo "⚠️ 检测到顶级赋值,注意是否修改了全局状态: $line"
# 可设置为警告而非阻断
done
检查测试覆盖率是否达标(进阶方案)
需要先运行测试,适合在 pre-push 钩子中执行(耗时较长)。
#!/bin/bash
# 需要事先配置 pytest-cov
MIN_COVERAGE=80
# 运行测试并获取覆盖率
coverage_output=$(pytest --cov=src --cov-report=term 2>&1)
coverage_value=$(echo "$coverage_output" | grep 'TOTAL' | awk '{print $4}' | sed 's/%//')
if [ "$coverage_value" -lt "$MIN_COVERAGE" ]; then
echo "❌ 测试覆盖率 ${coverage_value}% 低于阈值 ${MIN_COVERAGE}%"
exit 1
fi
检查 Mock 友好性(静态模式扫描)
检测是否直接实例化依赖类(而非通过工厂/DI)。
#!/bin/bash
# 检查是否有 "= MyService()" 这样直接实例化的模式
git diff --cached -U0 | grep '^+' | grep -E '= [A-Z]\w+\(\)' | \
while read line; do
echo "⚠️ 直接实例化检测: $line"
echo " 建议通过依赖注入或工厂方法创建,便于测试时 Mock"
# 可以设置 exit 1 来严格阻断
done
组合脚本:完整的 pre-commit 可测试性检查
#!/bin/bash
echo "===== 开始可测试性检查 ====="
errors=0
# 1. 检查测试文件
changed_files=$(git diff --cached --name-only | grep '\.py$' | grep -v '__init__' | grep -v 'test_')
for file in $changed_files; do
base=$(basename "$file" .py)
test_file="tests/test_${base}.py"
if [ ! -f "$test_file" ]; then
echo "❌ 缺少测试: $file -> 期望 $test_file"
errors=$((errors+1))
fi
done
# 2. 检查代码异味
git diff --cached -U0 | grep '^+' | egrep '(global |open\(|requests\.get|\.connect\()' | while read line; do
echo "⚠️ 不可测试模式: $line"
done
# 3. 检查函数长度(简化版)
git diff --cached --name-only | grep '\.py$' | xargs -r awk '
/^def /{if(func_lines>max) print FILENAME ":" func_name ":" func_lines; func_lines=0; func_name=$0}
/^ /{func_lines++}
' 2>/dev/null | while IFS=: read file func lines; do
if [ "$lines" -gt 60 ]; then
echo "❌ 函数过长 ($lines行): $file -> $func"
errors=$((errors+1))
fi
done
if [ $errors -gt 0 ]; then
echo "❌ 发现 $errors 个可测试性问题,提交拒绝"
exit 1
fi
echo "✅ 可测试性检查通过"
安装与使用
将上述脚本保存为 .git/hooks/pre-commit(或使用工具如 husky/pre-commit):
chmod +x .git/hooks/pre-commit
团队共享(推荐使用 pre-commit 框架):
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: check-testability
name: Check code testability
entry: ./scripts/check-testability.sh
language: script
stages: [commit]
注意事项
| 检查项 | 能检测 | 不能检测 |
|---|---|---|
| 测试文件存在 | 是否有同名测试 | 测试是否有效 |
| 函数长度 | 复杂函数 | 测试友好性 |
| 硬编码依赖 | requests.get() |
间接依赖注入 |
| 全局状态 | global 声明 |
通过模块变量 |
| 覆盖率 | 总体百分比 | 分支覆盖质量 |
最佳实践:
- 优先阻断硬编码依赖和过长函数(可测试性杀手)
- 测试文件缺失作为警告而非阻断(避免迁就 legacy 代码)
- 结合 CI 中的实际覆盖率门禁(更严格)
- 定期更新检查规则以适应团队技术栈变化
这样的钩子可以防止大量“写完代码发现无法写单元测试”的情况,帮助团队养成可测试性思维。