Claude Code 写测试实战:TDD、Mock、覆盖率一条龙

📘 AI教程 💬 🔥 Trending

🩺 摘要

说到写测试,大部分开发者的真实反应是"知道该写,但就是不想写"。不是测试没用,是手工写测试太枯燥——搞 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 会分析 PaymentGatewayEmailService 的接口签名,自动生成正确的 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 香港能用吗?