一、为什么要摆脱外部依赖?
1.1 外部依赖带来的麻烦
平时写代码时,是不是常遇到这种情况:写好的功能需要调用第三方接口、操作数据库、甚至调用其他服务的接口,这些外部依赖一旦出问题,你的单元测试直接崩掉——明明自己的代码逻辑没问题,却因为别人的服务挂了、本地数据库连不上,测试根本跑不起来。更糟的是,多人协作环境里,别人跑你的测试可能因为本地环境没配置好,根本无法启动测试,彻底失去了单元测试快速反馈的意义。
1.2 pytest-mock能做啥?
pytest-mock是pytest的实用插件,它封装了unittest.mock的功能,让Mock、Spy的使用更简洁。简单说,它可以把你代码里用到的外部对象(比如API调用方法、数据库连接对象)替换成可控的“假对象”,这些假对象只会按你设定的规则返回结果,不会真的调用外部服务或操作本地资源,让单元测试彻底和外部环境解耦,不管外部怎么变,你的测试都能稳定运行,精准验证自身代码逻辑。
二、Mock和Spy的核心用法
2.1 Mock的正确打开方式
Mock的核心是“替换依赖”,把你需要的外部依赖换成模拟对象,你可以给它设定固定返回值,或抛出指定异常,让测试只聚焦自身代码。
# 技术栈:Python + pytest + pytest-mock
# 待测试功能:根据城市判断是否适合户外运动
import requests
def is_fit_for_outdoor(city: str) -> bool:
# 调用第三方天气API获取温度
response = requests.get(f"https://api.weather.com/today?city={city}")
temperature = response.json()["temp"]
# 温度15-30度适合户外运动
return 15 <= temperature <= 30
测试这个功能时,不需要真的调用天气API,用Mock替换requests.get即可:
# 技术栈:Python + pytest + pytest-mock
import pytest
from your_module import is_fit_for_outdoor
def test_fit_for_outdoor(mocker):
# Step1:用mocker.patch替换待测试代码中引用的requests.get
mock_get = mocker.patch("requests.get")
# Step2:设定Mock对象的返回结构,模拟API返回的数据
mock_response = mock_get.return_value
mock_response.json.return_value = {"temp": 25}
# Step3:调用待测试函数,验证结果
result = is_fit_for_outdoor("北京")
assert result is True
# Step4:验证Mock是否按预期被调用(确认API参数正确)
mock_get.assert_called_once_with("https://api.weather.com/today?city=北京")
2.2 Spy的妙用场景
Spy和Mock不同,Mock是完全替换,Spy是“监听+保留原逻辑”——它不会篡改原函数的功能,但会记录函数的调用次数、传入参数、返回值,你既能用到原函数的逻辑,又能根据记录验证调用是否正确。 修改刚才的待测试函数,新增调用计数功能:
# 技术栈:Python + pytest + pytest-mock
# 待测试功能:带调用计数的户外判断
import requests
call_count = 0
def is_fit_for_outdoor_with_count(city: str) -> bool:
global call_count
call_count += 1
response = requests.get(f"https://api.weather.com/today?city={city}")
temperature = response.json()["temp"]
return 15 <= temperature <= 30
用Spy测试计数和调用情况:
# 技术栈:Python + pytest + pytest-mock
def test_call_count_with_spy(mocker):
# Step1:用Spy包装requests.get,保留原逻辑且监听调用
spy_get = mocker.spy(requests, "get")
# 重置全局计数,避免其他测试干扰
global call_count
call_count = 0
# Step2:调用待测试函数两次
is_fit_for_outdoor_with_count("北京")
is_fit_for_outdoor_with_count("上海")
# Step3:验证计数和调用参数是否符合预期
assert call_count == 2
spy_get.assert_any_call("https://api.weather.com/today?city=北京")
spy_get.assert_any_call("https://api.weather.com/today?city=上海")
三、实际开发中的应用场景
3.1 第三方API调用测试
几乎所有项目都会用到第三方API,比如支付、短信、物流接口,测试时用Mock替换这些API,不用真的充值或调用真实服务,就能验证不同返回场景下的代码逻辑(比如支付成功、失败、超时等情况)。 比如测试支付回调函数:
# 技术栈:Python + pytest + pytest-mock
def handle_payment_callback(order_id: str, status: str) -> bool:
# 调用订单系统接口更新状态
response = requests.post("https://api.order.com/update", json={"order_id": order_id, "status": status})
return response.status_code == 200
用Mock测试:
def test_payment_callback(mocker):
mock_post = mocker.patch("requests.post")
mock_post.return_value.status_code = 200 # 模拟订单系统返回成功
result = handle_payment_callback("12345", "success")
assert result is True
mock_post.assert_called_once_with("https://api.order.com/update", json={"order_id":"12345", "status":"success"})
3.2 数据库操作的隔离测试
如果测试涉及数据库操作,用真实数据库需要提前插数据、测试后清理,还可能污染正式数据。用Mock替换数据库连接,就能完全脱离数据库,测试速度快且稳定。 比如测试用户积分查询函数:
# 技术栈:Python + pytest + pytest-mock
import sqlite3
def check_user_points(user_id: int) -> bool:
conn = sqlite3.connect("user.db")
cursor = conn.cursor()
cursor.execute("SELECT points FROM users WHERE id=?", (user_id,))
points = cursor.fetchone()[0]
conn.close()
return points >= 100
用Mock测试:
def test_user_points(mocker):
mock_connect = mocker.patch("sqlite3.connect")
mock_cursor = mock_connect.return_value.cursor.return_value
mock_cursor.fetchone.return_value = (150,) # 模拟用户积分150
result = check_user_points(1)
assert result is True
mock_connect.assert_called_once_with("user.db")
四、技术优缺点和注意事项
4.1 用Mock/Spy的好处
第一,隔离外部依赖,测试速度大幅提升,不用等外部服务响应,也不会因外部服务不可用导致测试失败;第二,精准控制测试场景,比如想测试API返回异常时的代码逻辑,直接用Mock让它抛出异常即可,不用额外搭建测试环境;第三,通过Spy记录调用情况,能验证代码调用外部服务的方式是否正确,比如参数是否正确、调用次数是否符合预期。
4.2 要避开的坑
首先,Mock的路径必须正确,很多新手容易踩坑:Mock的路径应该是待测试代码中实际导入的模块路径,不是原模块路径,比如代码里导入的是moduleA.requests,Mock就要用moduleA.requests,不能用其他路径;其次,不要过度使用Mock,如果代码逻辑涉及多个函数的交互,小范围真实测试比Mock更靠谱,否则会出现Mock结果和真实运行不一致的情况;第三,Mock的返回值结构必须和真实情况一致,比如真实API返回字典,你Mock成列表,实际运行时就会报错,这点要格外注意。
五、总结
pytest-mock是Python单元测试中非常实用的工具,掌握Mock和Spy的用法,能帮你彻底摆脱外部依赖的束缚,写出更稳定、更快的单元测试。核心逻辑是:Mock用来替换依赖做控制,Spy用来监听依赖做验证,根据不同场景选择合适的方式,同时注意Mock路径和返回值的正确性,就能大幅提升测试效率,让协作时不再为测试环境的问题头疼。
Comments