当你的代码依赖外部服务(如数据库、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 测试和覆盖率统计,你的测试工作就迈入了专业阶段:不仅“写了测试”,而且能有效隔离依赖,量化测试质量,让重构和维护更有信心。