人人都会AI编程

24.3 Mock 测试、测试覆盖率统计

更新时间:2026-07-12

当你的代码依赖外部服务(如数据库、HTTP API、文件系统)或其他尚未完成的模块时,直接写测试会遇到两个问题:速度慢不可控。Mock 测试就是专门解决这个问题的技术。写完测试之后,我们还需要知道到底测了哪些代码,这就要靠测试覆盖率统计。

Mock 测试:替换依赖,隔离被测对象

Mock 的核心思想是:用一个“假的”对象替换真实的依赖,让测试只验证当前单元的逻辑,而不真的去调用外部系统。

Python 标准库的 unittest.mock 提供了强大的 Mock 能力,而 pytest 推荐直接使用它,或者通过 pytest-mock 插件更方便地使用。

1. 基本用法
假设你有一个函数,内部调用了第三方 API:

# weather.py
import requests

def get_temperature(city):
    url = f"https://api.weather.com/v1/current?city={city}"
    resp = requests.get(url, timeout=5)
    data = resp.json()
    return data["current"]["temp"]

测试这个函数,我们不想每次真去联网,所以 Mock 掉 requests.get

from unittest.mock import patch
from weather import get_temperature

def test_get_temperature():
    # 用 patch 临时替换 requests.get
    with patch("weather.requests.get") as mock_get:
        # 配置 Mock 对象的返回值
        mock_get.return_value.json.return_value = {
            "current": {"temp": 23.5}
        }
        
        result = get_temperature("Beijing")
        
        # 断言返回值正确
        assert result == 23.5
        # 断言 API 被正确调用了一次
        mock_get.assert_called_once_with(
            "https://api.weather.com/v1/current?city=Beijing",
            timeout=5
        )

关键点:

  • patch 的参数是“目标函数的完整路径字符串”,也就是在被测代码中实际导入使用的名字,而不是定义的地方。
  • mock_get.return_value 是 Mock 对象的返回值(即 resp),我们进一步设置它的 .json() 返回值。
  • 除了断言结果,还可以断言调用次数、调用参数,确保交互正确。

2. 使用 pytest-mock 插件
安装 pytest-mock 后,你会获得一个叫 mocker 的 fixture,更简洁:

def test_get_temperature(mocker):
    mock_get = mocker.patch("weather.requests.get")
    mock_get.return_value.json.return_value = {
        "current": {"temp": 23.5}
    }
    # ... 其余相同

mocker 的使用方式与 unittest.mock.patch 类似,但作用域更贴合 pytest,也更方便在多个测试中重复使用。

3. Mock 常见场景

  • 替换外部 API:如上例,不发起网络请求。
  • 替换数据库调用:用 Mock 对象代替 cursor.execute 或 ORM 的查询方法,返回预设数据,让测试秒级完成。
  • 替换时间、随机数等不可控因素:比如 patch("time.time") 固定返回一个时间戳,验证时间相关逻辑。
  • 隔离未完成的模块:被测代码依赖的其他模块还没写好,可以先 Mock 其接口,继续推进测试。

4. 注意事项

  • 不要 Mock 过度:只 Mock 真正影响测试速度和稳定性的外部依赖,避免连自身函数都 Mock 了,导致测试失去意义。
  • Mock 的位置很重要:必须精确替换被测代码实际引用的位置,不是库的源头。这是新手容易踩的坑。
  • 组合 Mock 与真实集成测试:Mock 测试保证单元逻辑正确,最终仍需少量集成测试验证整体交互。

测试覆盖率统计

写完测试后,你会问:“我的测试到底测了多少代码?” 测试覆盖率就是回答这个问题的量化指标。

1. 核心概念

  • 行覆盖率:被执行的代码行数占总行数的百分比。
  • 分支覆盖率:比如 if/else 两个分支,都测到了才算完全覆盖。
  • 常见目标:很多团队要求 80%-90% 的行覆盖率,但更重要的是关键业务逻辑必须全部覆盖,而不是盲目追求数字。

2. 使用 pytest-cov 统计覆盖率
最方便的工具是 pytest-cov(内部封装了 coverage.py)。

安装

pip install pytest-cov

运行测试并生成覆盖率报告

pytest --cov=my_project tests/

--cov 指定要统计的包名(你的代码目录)。

生成终端报告

pytest --cov=my_project --cov-report=term-missing tests/

term-missing 会在终端列出具体未覆盖的代码行,方便定位遗漏。

生成 HTML 报告(推荐,可视化强):

pytest --cov=my_project --cov-report=html tests/

运行后打开 htmlcov/index.html,可以逐文件、逐行查看覆盖情况,绿色是已覆盖,红色是未覆盖。

3. 覆盖率配置文件 .coveragerc
可以在项目根目录下创建 .coveragerc 文件,排除测试自身和某些不关心的目录:

[run]
source = my_project
omit = 
    */tests/*
    */migrations/*
    */__init__.py

这样就不用每次在命令行指定一堆参数。

4. 覆盖率不是唯一标准

  • 高覆盖率 ≠ 测试质量高:你完全可能写一堆只跑代码却无断言的测试。关键是测试用例是否真正验证了业务逻辑。
  • 合理忽略:像 main 入口、配置常量、简单的 getter/setter 等,不一定非要覆盖,但如果核心分支没测,就必须补上。
  • 迭代提升:从无到有先测关键路径,后续迭代中逐步补充,而不是追求一次性 100%。

5. 实践建议

  • 在 CI/CD 流程中集成覆盖率检查,设定最低阈值(比如 80%),低于阈值则流程失败,保证新提交的代码都有基本测试。
  • 定期查看未覆盖代码行,反思:这些代码真的是“安全不用测”的吗?还是确实漏掉了重要逻辑?
  • Mock 测试和覆盖率结合使用时,注意 Mock 外部依赖后,被 Mock 的那部分调用不会被真实执行,这部分通常我们也不要求覆盖率,应排除在统计外。合理利用 .coveragerc 或在代码块加 # pragma: no cover 注释标记例外。

掌握 Mock 测试和覆盖率统计,你的测试工作就迈入了专业阶段:不仅“写了测试”,而且能有效隔离依赖,量化测试质量,让重构和维护更有信心。