名称: 测试质量检查员 描述: 系统化地检查单元和端到端测试以验证它们测试正确的行为,提供有意义的覆盖,并捕获真实的回归 使用时机: 工程师创建测试后、标记QA完成前、当测试失败可疑时、或当测试通过但仍有bug时 版本: 1.0.0 渐进式披露: 入口点: 摘要: “检查测试以验证它们实际上测试所声称的内容,捕获真实失败,并提供有意义的断言。” 使用时机: “测试编写后、QA签字前、调查测试失败时、或生产bug逃逸时。” 快速开始: “1. 阅读测试 2. 问:‘这个测试在测试什么行为?’ 3. 验证断言匹配意图 4. 检查失败场景 5. 提出改进建议” 参考: - inspection-checklist.md - assertion-quality.md - coverage-analysis.md - red-flags.md
测试质量检查员
目的
工程师经常编写测试,它们通过但并未真正测试正确的东西。本技能提供系统化检查技术,以验证测试是否:
- 测试真实行为,而非实现细节
- 做出有意义的断言
- 捕获实际失败
- 提供价值,不仅仅是覆盖
何时使用本技能
在以下时机激活本技能:
- 工程师创建测试后 - 验证测试有效
- QA签字前 - 未经检查不批准测试
- 测试失败可疑时 - “为什么没抓到bug?”
- 测试通过但仍有bug时 - 测试未测试正确的东西
- 覆盖率高但信心低时 - 覆盖率 ≠ 质量
- 重构现有测试时 - 提高测试有效性
检查框架
阶段1: 意图分析
问题: 这个测试应该验证什么?
1. 阅读测试名称
2. 阅读测试描述/docstring
3. 识别声称的行为
4. 记录预期结果
红色旗帜:
- 模糊的测试名称(“test_user_creation”)
- 没有预期行为的描述
- 测试以方法命名,而非行为
- “测试X是否工作”("工作"是什么意思?)
阶段2: 设置分析
问题: 测试设置是否现实?
1. 检查测试夹具/设置
2. 检查数据真实性
3. 验证状态初始化
4. 识别模拟/存根
红色旗帜:
- 永远不会发生的模拟数据
- 缺失必需的依赖
- 过度简化的场景
- 绕过真实约束的设置
阶段3: 执行分析
问题: 测试是否锻炼真实行为?
1. 追踪执行路径
2. 识别实际调用的内容
3. 检查模拟是否覆盖真实行为
4. 验证集成点
红色旗帜:
- 模拟被测试系统
- 测试模拟行为而非真实行为
- 执行路径不匹配意图
- 关键代码路径未锻炼
阶段4: 断言分析
问题: 断言是否验证声称的行为?
1. 检查每个断言
2. 映射断言到意图
3. 检查断言强度
4. 验证失败条件
红色旗帜:
- 弱断言(“assert result is not None”)
- 断言存在性,而非正确性
- 缺失错误情况的断言
- 断言在模拟输出上,而非真实输出
阶段5: 失败分析
问题: 什么会让这个测试失败?
1. 引入有意的bug
2. 检查测试是否捕获它们
3. 验证错误信息是否有意义
4. 明确测试失败场景
红色旗帜:
- 测试与明显损坏的代码一起通过
- 移除功能不导致测试失败
- 错误信息不指示失败原因
- 无负面测试用例
检查清单
为每个检查的测试使用此清单:
意图验证
- [ ] 测试名称描述行为,而非实现
- [ ] 预期行为明确声明
- [ ] 成功标准明确
- [ ] 测试有单一明确目的
设置质量
- [ ] 测试数据现实
- [ ] 必需的依赖存在
- [ ] 状态初始化有效
- [ ] 模拟有理由且完整
执行质量
- [ ] 真实代码路径被锻炼
- [ ] 被测试系统未模拟
- [ ] 集成点被测试
- [ ] 副作用被验证
断言质量
- [ ] 断言匹配声明的意图
- [ ] 断言具体且有意义
- [ ] 成功和失败案例都被测试
- [ ] 错误条件被验证
回归预防
- [ ] 测试会捕获已知bug模式
- [ ] 边界条件被测试
- [ ] 边缘情况被覆盖
- [ ] 失败模式明确
常见测试异味
1. 乐观断言者
# 错误: 只测试快乐路径
def test_user_login():
user = create_user("test@example.com", "password123")
assert user is not None # 弱!
问题:
- 未测试登录是否实际工作
- 弱断言(not None)
- 缺失:错误密码、锁定账户等
改进:
def test_user_login_with_valid_credentials_returns_authenticated_session():
user = create_user("test@example.com", "password123")
session = login(user.email, "password123")
assert session.is_authenticated
assert session.user_id == user.id
assert session.expires_at > datetime.now()
def test_user_login_with_invalid_password_raises_authentication_error():
user = create_user("test@example.com", "password123")
with pytest.raises(AuthenticationError) as exc:
login(user.email, "wrong_password")
assert "Invalid credentials" in str(exc.value)
2. 模拟测试者
# 错误: 测试模拟行为
def test_email_sending():
mock_smtp = Mock()
mock_smtp.send.return_value = True
result = send_email(mock_smtp, "test@example.com", "Hello")
assert mock_smtp.send.called # 测试模拟!
assert result is True # 模拟的返回值!
问题:
- 测试模拟行为,而非真实邮件发送
- 无验证实际邮件内容
- 模拟配置为总是成功
改进:
def test_email_sending_with_test_smtp_server():
"""使用真实SMTP测试服务器或捕获实际邮件"""
with captured_emails() as outbox:
result = send_email("test@example.com", "Hello", "Test body")
assert result.success
assert len(outbox) == 1
email = outbox[0]
assert email.to == ["test@example.com"]
assert email.subject == "Hello"
assert "Test body" in email.body
3. 实现测试者
# 错误: 测试实现细节
def test_user_password_storage():
user = User("test@example.com", "password123")
assert user._password_hash.startswith("$2b$") # bcrypt细节
assert len(user._password_hash) == 60
问题:
- 测试私有实现
- 如果哈希算法改变会中断
- 未测试实际密码验证
改进:
def test_user_password_verification_accepts_correct_password():
user = User("test@example.com", "password123")
assert user.verify_password("password123") is True
def test_user_password_verification_rejects_incorrect_password():
user = User("test@example.com", "password123")
assert user.verify_password("wrong") is False
4. 假阳性者
# 错误: 总是通过的测试
def test_data_validation():
validator = DataValidator()
result = validator.validate({"name": "Test"})
assert result # 这证明了什么?
问题:
- 即使验证损坏也会通过
- 未验证验证了什么
- 无负面案例
改进:
def test_data_validation_accepts_valid_data():
validator = DataValidator()
result = validator.validate({"name": "Test", "age": 25})
assert result.is_valid is True
assert result.errors == []
def test_data_validation_rejects_missing_required_field():
validator = DataValidator()
result = validator.validate({"age": 25}) # 缺失'name'
assert result.is_valid is False
assert "name" in result.errors
assert "required" in result.errors["name"].lower()
def test_data_validation_rejects_invalid_field_type():
validator = DataValidator()
result = validator.validate({"name": "Test", "age": "not a number"})
assert result.is_valid is False
assert "age" in result.errors
检查工作流程
步骤1: 阅读测试
1. 测试名称声称测试什么?
2. 描述/docstring是什么?
3. 设置是什么?
4. 执行是什么?
5. 断言什么?
步骤2: 验证意图匹配
1. 执行是否匹配测试名称?
2. 断言是否验证声称的行为?
3. 边缘情况是否覆盖?
4. 错误案例是否测试?
步骤3: 检查断言强度
1. 这个断言能有效失败吗?
2. 是否验证正确性,不仅仅是存在性?
3. 是否会捕获回归?
4. 错误信息有帮助吗?
步骤4: 执行思维调试
1. 引入bug:这个测试会捕获吗?
2. 移除功能:这个测试会失败吗?
3. 打破约束:这个测试会注意到吗?
4. 返回错误数据:断言会捕获吗?
步骤5: 提出改进建议
格式:
"这个测试声称验证[X]但实际上测试[Y]。
问题:
- 断言太弱: [具体问题]
- 缺失[失败案例]的测试
- 设置不现实: [具体问题]
建议改进:
1. [具体更改及示例]
2. [需要额外的测试案例]
3. [断言加强]
"
质量指标
断言质量评分
弱: assert x is not None
中等: assert len(x) > 0
强: assert x == expected_value
最强: assert x.field == expected AND verify_invariants(x)
测试覆盖 vs 测试质量
仅覆盖率是无意义的:
- 100%覆盖率与弱断言 = 虚假信心
- 80%覆盖率与强测试 = 更好保护
质量指标:
✓ 测试特定行为,而非方法
✓ 断言验证正确性
✓ 失败案例明确
✓ 测试会捕获已知bug
与其他技能集成
相关技能
- testing-anti-patterns - 什么不该做(避免常见陷阱)
- test-driven-development - 如何先写测试(预防弱测试)
- webapp-testing - 特定端到端测试检查技术
- condition-based-waiting - 适当异步测试模式
工作流程集成
1. 工程师编写测试
2. QA使用本技能检查
3. QA提出改进建议
4. 工程师改进测试
5. QA验证改进
6. QA签字
红色旗帜摘要
如果看到以下立即调查:
🚩 测试名称: “test_method_name”, “test_works”, “test_user” 🚩 断言: “assert x”, “assert result”, “assert not None” 🚩 模拟: 模拟被测试系统、不理解就模拟 🚩 覆盖: 高覆盖率但信心低 🚩 失败: 测试通过但生产中有bug 🚩 信息: 测试中断时不清楚失败原因 🚩 设置: 现实永远不会发生的测试数据
快速参考
好测试特征
- ✅ 清晰、行为聚焦的名称
- ✅ 具体、有意义的断言
- ✅ 测试真实代码路径
- ✅ 覆盖成功和失败案例
- ✅ 会捕获回归
- ✅ 有帮助的失败信息
坏测试特征
- ❌ 模糊名称(“test_user”)
- ❌ 弱断言(not None)
- ❌ 测试模拟行为
- ❌ 仅快乐路径
- ❌ 不会捕获bug
- ❌ 神秘失败
示例检查报告
### 测试: test_user_creation()
**声称意图:** 测试用户创建
**实际测试:** 仅对象实例化
**发现问题:**
1. 弱断言: `assert user is not None`
- 即使用户数据损坏也会通过
- 未验证任何用户属性
2. 缺失验证测试:
- 无重复邮箱测试
- 无效邮箱格式测试
- 密码要求测试
3. 无数据库持久性检查:
- 用户可能未保存
- 测试未验证检索
**建议改进:**
1. 重命名为: `test_user_creation_with_valid_data_persists_to_database`
2. 加强断言:
```python
assert user.email == "test@example.com"
assert user.is_active is True
assert user.created_at is not None
# 验证持久性
retrieved = User.get_by_email("test@example.com")
assert retrieved.id == user.id
- 添加负面测试:
- test_user_creation_with_duplicate_email_raises_error
- test_user_creation_with_invalid_email_raises_error
- test_user_creation_with_weak_password_raises_error
风险等级: 高 - 核心功能未实际测试 推荐: 直到改进才批准合并
## 记住
> "一个总是通过的测试根本不是测试。"
> "覆盖率测量运行的行,而非验证的正确性。"
> "如果移除功能不导致测试失败,这个测试毫无价值。"
作为QA,你的工作不是批准存在的测试,而是验证保护性的测试。