一、为什么要摆脱外部依赖?

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路径和返回值的正确性,就能大幅提升测试效率,让协作时不再为测试环境的问题头疼。