Claude Code 写测试实战:TDD、Mock、覆盖率一条龙
🩺 摘要
说到写测试,大部分开发者的真实反应是"知道该写,但就是不想写"。不是测试没用,是手工写测试太枯燥——搞 Mock 数据、...
📝 详情
说到写测试,大部分开发者的真实反应是"知道该写,但就是不想写"。不是测试没用,是手工写测试太枯燥——搞 Mock 数据、调断言、追覆盖率百分比,这些活重复又费脑。Claude Code 最被低估的能力不是写业务代码,而是写测试。本文不讲大道理,直接上实操:怎么用 Claude Code 做 TDD、生成 Mock、追覆盖率,让写测试从"苦差"变成"顺手的事"。
一、TDD 模式:先写测试,再写代码
TDD(测试驱动开发)的核心:先写会失败的测试,再写代码让它通过。Claude Code 对 TDD 天然友好,因为它不需要你手动切换上下文。
### 1.1 让 Claude 先写测试
典型 TDD 流程,找一个空的 calculator.py,直接告诉 Claude:
bash
claude "对这个 calculator 模块做 TDD。先写 pytest 测试,再实现功能。
要求:
- add(a, b) 返回两数之和
- subtract(a, b) 返回 a - b
- multiply(a, b) 返回乘积
- divide(a, b) 返回除法结果,除零抛 ValueError
- 测试覆盖正常值和边界值"
Claude 会先创建 test_calculator.py,再创建 calculator.py,所有测试通过。
效果对比:
| 步骤 | 传统 TDD | Claude Code TDD |
|---|---|---|
| 写测试 | 10分钟 | 30秒(Claude 自动) |
| 实现功能 | 15分钟 | 20秒(Claude 自动) |
| 跑测试 | 5秒 | 5秒 |
| 重构 | 10分钟 | 30秒(Claude 自动) |
| 合计 | ~35分钟 | ~1.5分钟 |
### 1.2 存量代码补测试
更常见的场景:项目写了一半,现在要补测试。告诉 Claude:
bash
claude -p "对现有的 models/user.py 写 pytest 测试:
- 覆盖所有公有方法
- Mock 数据库连接(不要真的连数据库)
- 测试正常流程和错误流程
- 覆盖率目标 90%+
- 用 pytest-cov 验证覆盖率"
Claude 会读 models/user.py 的源码,分析函数签名和逻辑分支,自动生成对应的测试文件。
关键技巧:给 Claude 看具体代码比描述更准。用 @ 引用文件:
bash
claude -p "为 @models/user.py 写单元测试。使用 pytest + unittest.mock"
## 二、Mock 实战:别让测试碰真实依赖
写测试最头疼的事:被测函数调了数据库、发了 HTTP 请求、访问了文件系统。Mock 就是替代这些外部依赖的"替身",让测试只测当前函数的逻辑。
### 2.1 让 Claude 自动写 Mock
bash
claude -p "为 @services/payment.py 写测试:
- Mock PaymentGateway.send() 方法
- Mock EmailService.notify() 方法
- 验证在支付成功时发送通知邮件
- 验证支付失败时调用 retry 逻辑
- 用 pytest monkeypatch 或 unittest.mock"
Claude 会分析 PaymentGateway 和 EmailService 的接口签名,自动生成正确的 Mock 代码。
### 2.2 典型 Mock 场景模板
HTTP 请求 Mock:
```python # Claude 生成的代码示例 import pytest from unittest.mock import patch
def test_fetch_user_data(): mock_response = {"id": 1, "name": "Test User", "email": "test@example.com"}
with patch("services.user.requests.get") as mock_get:
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = mock_response
result = fetch_user_data(1)
assert result["name"] == "Test User"
mock_get.assert_called_once_with("https://api.example.com/users/1")
```
数据库 Mock:
```python @pytest.fixture def mock_db(): with patch("models.user.get_db_connection") as mock_conn: mock_cursor = MagicMock() mock_conn.cursor.return_value = mock_cursor mock_cursor.fetchone.return_value = {"id": 1, "name": "Alice"} yield mock_conn
def test_get_user_by_id(mock_db): user = get_user_by_id(1) assert user["name"] == "Alice" ```
文件系统 Mock:
```python def test_process_csv(): mock_content = "name,age\nAlice,30\nBob,25"
with patch("builtins.open", mock_open(read_data=mock_content)):
result = process_csv("users.csv")
assert len(result) == 2
assert result[0]["name"] == "Alice"
```
### 2.3 Mock 最佳实践(让 Claude 遵守)
把以下规则写进 CLAUDE.md:
```markdown ## Testing Conventions
- 使用 pytest + unittest.mock
- Mock 外部依赖(DB、HTTP、文件系统)
- 一个测试只测一件事
- 测试函数名用 test_<功能>_<场景> 格式
- Fixture 放在 conftest.py ```
这样每次生成测试都会自动遵守团队规范。
## 三、覆盖率优化:让 Claude 帮你追数字
### 3.1 自动扫描覆盖率缺口
```bash # 先跑覆盖率 pytest --cov=./src --cov-report=term-missing
# 把结果喂给 Claude pytest --cov=./src --cov-report=json claude "分析 coverage.json,找出覆盖率低于 80% 的模块, 为每个模块建议需要补充哪些测试场景。" ```
### 3.2 针对低覆盖文件补测试
bash
claude -p "查看 @src/utils/validator.py 的覆盖率报告,
为所有未覆盖的分支补充测试:
- if/else 的每个分支
- 异常处理路径
- 边界值(空字符串、None、超大值)
- 用 pytest-cov 验证补充后覆盖率达标"
### 3.3 覆盖率阶梯策略
一次性追到 100% 不现实。分阶梯来:
```bash # 第一阶段:核心逻辑 claude -p "为 @src/core/ 下所有文件写测试,目标 80% 覆盖率"
# 第二阶段:关键路径 claude -p "为 @src/api/ 下所有路由处理函数写测试,目标 85%"
# 第三阶段:配置文件 claude -p "为 @src/config/ 下所有配置解析函数写测试,目标 70%" ```
## 四、真实项目:一个完整测试场景
假设有一个用户注册 register.py:
```python import re from typing import Optional import requests
class UserRegistration: def init(self, db_conn, email_service): self.db = db_conn self.email = email_service
def register(self, username: str, email: str, password: str) -> dict:
if not re.match(r'^[a-zA-Z0-9_]{3,20}$', username):
raise ValueError("Invalid username")
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email):
raise ValueError("Invalid email")
if len(password) < 8:
raise ValueError("Password too short")
if self.db.query("SELECT id FROM users WHERE email=?", (email,)):
raise ValueError("Email already registered")
hashed = self._hash_password(password)
user_id = self.db.execute(
"INSERT INTO users (username, email, password) VALUES (?,?,?)",
(username, email, hashed)
)
self.email.send_welcome(email, username)
return {"id": user_id, "username": username, "email": email}
def _hash_password(self, password: str) -> str:
import hashlib, os
salt = os.urandom(32)
return hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000).hex()
```
告诉 Claude:
bash
claude -p "为 @register.py 写完整的 pytest 测试:
- Mock db_conn 和 email_service
- 覆盖:正常注册、用户名无效、邮箱无效、密码过短、邮箱已注册
- 验证 email.send_welcome 在成功时被调用,失败时不被调用
- 用 pytest-cov 确认分支覆盖率 100%"
Claude 会生成类似这样的测试:
```python import pytest from unittest.mock import MagicMock, patch from register import UserRegistration
@pytest.fixture def registration(): db = MagicMock() email = MagicMock() return UserRegistration(db, email), db, email
def test_register_success(registration): reg, db, email = registration db.query.return_value = None # 邮箱未注册 db.execute.return_value = 42 # 返回新用户ID
result = reg.register("testuser", "test@example.com", "password123")
assert result["id"] == 42
assert result["username"] == "testuser"
db.execute.assert_called_once()
email.send_welcome.assert_called_once_with("test@example.com", "testuser")
def test_register_invalid_username(registration): reg, _, _ = registration with pytest.raises(ValueError, match="Invalid username"): reg.register("ab", "test@example.com", "password123")
def test_register_invalid_email(registration): reg, _, _ = registration with pytest.raises(ValueError, match="Invalid email"): reg.register("testuser", "not-an-email", "password123")
def test_register_short_password(registration): reg, _, _ = registration with pytest.raises(ValueError, match="Password too short"): reg.register("testuser", "test@example.com", "pw123")
def test_register_email_exists(registration): reg, db, email = registration db.query.return_value = {"id": 1} # 邮箱已注册
with pytest.raises(ValueError, match="Email already registered"):
reg.register("testuser", "test@example.com", "password123")
email.send_welcome.assert_not_called()
```
## 五、测试维护:当代码改了,测试怎么办
### 5.1 改代码时让 Claude 同步更新测试
bash
# 改完代码后
claude -p "我刚改了 @services/payment.py 的 process_refund 方法,
签名从 (amount) 改为 (amount, reason)。
更新 @tests/test_payment.py 中对应的测试。"
### 5.2 批量更新测试
bash
# 批量续命
claude -p "检查 @tests/ 目录下所有测试文件,
对每个文件运行 pytest --collect-only,
找出因为 API 变更而收集不到的错误测试,
根据 @src/ 中的新签名更新测试。"
### 5.3 测试代码本身也需要 review
bash
claude -p "review @tests/test_payment.py,检查:
- 是否有遗漏的测试场景
- Mock 是否正确(参数数量、返回值类型)
- 断言是否足够严格
- 是否有不必要的 Mock(可以不用 Mock 的不要 Mock)"
要点总结:
| 场景 | Command | 效果 |
|---|---|---|
| TDD 新功能 | claude "对 module.py 做 TDD,先写测试" |
自动生成测试+实现 |
| 存量补测试 | claude "为 @module.py 写测试" |
分析代码自动生成 |
| Mock 外部依赖 | claude "Mock DB 和 HTTP,不碰真实服务" |
自动 Mock 所有外部调用 |
| 追覆盖率 | claude "分析 coverage.json,补缺口" |
定位低覆盖区域 |
| 测试更新 | claude "我刚改了签名,更新测试" |
自动适配新签名 |
写测试的最大障碍不是技术,是"不想开始"。Claude Code 把这个障碍降到了零——你只需要说一句"写测试",它就把该写的都写好了。
系列其他文章: - 实战:10分钟用AI搭建网页应用 - 进阶技巧:CLAUDE.md与MCP配置 - 避坑指南:新手10个错误 - 2026 AI编程工具选购指南 - Claude Code 团队协作实战 - Claude Code CI/CD 集成 - Claude Code 香港能用吗?
💬 评论 (0)