pytest-mock实战指南:高效隔离外部依赖的7大场景

发布时间:2026/7/6 22:02:50

pytest-mock实战指南:高效隔离外部依赖的7大场景 1. 为什么我坚持用 pytest-mock 而不是直接写 unittest.mock你刚接手一个老项目跑一遍测试要花 47 秒——其中 32 秒在等数据库连接超时、8 秒在调用模拟支付网关的沙箱接口、还有 3 秒在等 Redis 响应。你改了一行日志输出却要为这 47 秒买单。这不是开发这是守夜。这就是我第一次认真看pytest-mock文档的起点。它不是“又一个测试工具”而是把“测试不该有的开销”从根上砍掉的手术刀。我试过纯unittest.mock手写patch的start()和stop()在setUp/tearDown里反复注册注销mock 对象嵌套三层后连自己都记不清mock_a.return_value.mock_b.side_effect到底该配在哪一层。更糟的是当同事在另一个测试里 patch 同一个模块两个测试一并行就互相污染——你改完自己的测试他的测试突然 fail谁都不明白为什么。pytest-mock的mockerfixture 解决的从来不是“能不能 mock”而是“敢不敢写测试”。它让 mock 变成和tmp_path、caplog一样自然的测试资源声明即用作用域清晰自动清理。你不用操心 patch 的生命周期不用手动 restore甚至不用 importMock——mocker.Mock()就是它家亲儿子。我见过最典型的场景一个处理用户注册的函数要调用邮件服务、短信网关、风控 API 三处外部依赖。用原生 mock 写测试光 setup 就占去 20 行用mocker核心逻辑 5 行搞定其余全是 assert。这不是偷懒是把注意力从“怎么让测试跑起来”拉回到“这个函数到底该做什么”。它背后真正的价值在于把“测试隔离性”从一项需要反复提醒的规范变成一种无需思考的肌肉记忆。当你写def test_send_welcome_email(mocker):时你潜意识已经接受了“这个测试只关心邮件发送逻辑其他都是噪音”的契约。这种心智模型的转变比任何语法糖都重要。所以别把它当成unittest.mock的包装器——它是 pytest 生态里为“快速、可靠、可读”量身定制的 mocking 语言。2. 从零搭建可复现的测试环境虚拟环境、依赖与验证2.1 为什么 conda 环境比 venv 更适合测试初学者很多教程一上来就让你python -m venv env但我在带新人时发现90% 的环境问题出在 Python 版本和系统库冲突上。比如 macOS 自带的 Python 3.9你装了pandas1.5它偷偷依赖numpy1.23而numpy1.23 在 Apple Silicon 上编译失败——这时候venv只会报错ModuleNotFoundError你得翻三天 issue 才知道是架构问题。Conda 的优势在于它管理的是“二进制包”不是源码。conda create -n pytest-demo python3.11这条命令执行后你得到的是一个预编译好的、经过全平台验证的 Python 3.11 环境。我实测过在 M1 Mac、Intel Ubuntu、Windows WSL 三个系统上用同一句 conda 命令创建的环境pytest --version输出完全一致。这不是巧合是 conda 社区对科学计算生态的长期打磨。提示如果你的公司禁用 conda常见于金融、政企环境请改用pyenvvirtualenv组合。pyenv install 3.11.8 pyenv virtualenv 3.11.8 pytest-demo pyenv activate pytest-demo效果接近 conda且不依赖额外包管理器。2.2 安装与验证三步确认你的环境真正干净别跳过验证步骤。我见过太多人以为pip install pytest pytest-mock就完事结果运行时提示ImportError: No module named pytest_mock——因为 pip 装到了系统 Python而你激活的是 conda 环境。第一步确认当前环境# 检查 Python 解释器路径 which python # 输出应为 /opt/anaconda3/envs/pytest-demo/bin/python 或类似路径 # 检查 pip 关联的 Python pip -V # 输出应包含 pytest-demo 字样第二步安装并验证版本兼容性pip install pytest7.0 pytest-mock3.10这里必须加版本约束。pytest-mock 4.0要求pytest8.0而pytest 8.0移除了pytest.config全局对象——如果你的旧项目里有pytest.config.getoption(--env)这种写法升级就会炸。我建议锁定pytest-mock3.10,4.0这是目前最稳定的黄金组合支持 Python 3.8-3.12 全版本。第三步终极验证——运行一个“空测试”创建test_smoke.pydef test_always_passes(): assert True然后执行pytest test_smoke.py -v --tbshort如果看到PASSED且没有ImportError说明环境已就绪。这一步看似多余但它能暴露 80% 的环境配置问题——比如pytest插件未加载、PYTHONPATH错乱、甚至 IDE 缓存导致的假成功。2.3 一个被严重低估的技巧用 requirements-test.txt 精确控制测试依赖很多人把所有依赖写在requirements.txt里结果生产环境部署时pytest-mock被意外装到线上服务器。正确做法是分离依赖requirements-test.txt内容-r requirements.txt pytest7.0,8.0 pytest-mock3.10,4.0 pytest-cov4.0 # 用于覆盖率报告这样做的好处是pip install -r requirements-test.txt只装测试所需避免污染生产环境CI/CD 流水线可以明确指定pip install -r requirements-test.txt杜绝版本漂移当你需要临时降级pytest比如调试某个插件兼容性只需改这一行不影响主依赖我维护的 12 个微服务项目全部采用此结构三年来零环境相关故障。3. 核心原理拆解mocker fixture 如何工作它和 unittest.mock 的本质区别3.1 mocker 不是魔法它是“自动化的 patch 管理器”翻开pytest-mock源码你会发现mockerfixture 的核心只有 200 行。它的本质是在每个测试函数执行前自动创建一个unittest.mock.Mock实例并在测试结束后自动调用patch.stopall()。但这个“自动”解决了三个致命痛点作用域泄漏原生patch必须手动start()/stop()。如果测试中途抛异常stop()不会被执行mock 就会污染下一个测试。mocker用 pytest 的addcleanup机制确保无论成功失败清理动作必执行。嵌套 patch 的地狱假设你要 mockrequests.get和os.path.exists原生写法patch(requests.get) patch(os.path.exists) def test_something(mock_exists, mock_get): pass参数顺序和 patch 顺序相反极易出错。而mocker直接def test_something(mocker): mock_get mocker.patch(requests.get) mock_exists mocker.patch(os.path.exists)顺序即执行顺序逻辑直白。跨模块 patch 的歧义patch(myapp.utils.logger)中的myapp.utils.logger是指“被 patch 的对象所在模块”还是“patch 动作发生的模块”新手永远分不清。mocker.patch()的第一个参数永远是“目标对象的完整路径”和你在代码里 import 的路径完全一致所见即所得。3.2 为什么 mocker.patch.object 比 mocker.patch 更安全看这个经典陷阱# 错误示范patch 模块中的对象 mocker.patch(myapp.database.connect) # ✅ 正确 mocker.patch(myapp.database.Connection) # ❌ 危险 # 正确做法始终 patch 类本身 mocker.patch.object(myapp.database, Connection)原因在于 Python 的导入机制。myapp.database.Connection是一个类但myapp.database模块里可能通过from .connection import Connection导入它。此时mocker.patch(myapp.database.Connection)实际 patch 的是myapp.database模块的__dict__而真实使用Connection的代码可能从myapp.connection导入——patch 失败。mocker.patch.object(target, attribute)强制你先import target再指定attribute从根本上规避了路径歧义。我团队的代码规范强制要求所有patch操作必须用patch.object除非 patch 的是标准库如time.sleep。3.3 mocker.spy 的真相它不是“监控”而是“增强版的原始函数”很多文档说spy是“监视函数调用”这容易误导。实际上mocker.spy(obj, method)返回的不是一个监控器而是一个完全替代原方法的新函数这个新函数在执行原逻辑前后自动记录调用信息。看这个例子class Calculator: def add(self, a, b): print(fAdding {a} and {b}) return a b def test_add_with_spy(mocker): calc Calculator() spy_add mocker.spy(calc, add) # 注意spy 是对实例方法不是类方法 result calc.add(2, 3) # 这里调用的是 spy_add不是原 add assert result 5 spy_add.assert_called_once_with(2, 3) # 关键点print 语句依然执行因为 spy_add 内部调用了原 addspy_add的实现伪代码是def spy_add(*args, **kwargs): record_call(args, kwargs) # 记录调用 original_result original_add(*args, **kwargs) # 执行原逻辑 record_result(original_result) # 记录返回值 return original_result所以spy的真正价值在于你既想验证函数被如何调用又不想破坏其实际行为。比如测试一个发邮件函数你既要确认sendmail()被调用又要确保邮件真发出去用于集成测试。这时spy就是唯一选择——patch会阻止邮件发送spy则让它照常工作。4. 实战详解从基础到高阶的 7 个关键场景4.1 场景一模拟外部 API 调用最常用也最容易踩坑假设你有一个获取天气数据的函数# weather.py import requests def fetch_weather(city: str) - dict: url fhttps://api.weather.com/v3/weather/forecast?city{city} response requests.get(url, timeout5) response.raise_for_status() return response.json()错误写法导致测试不稳定# ❌ 危险网络请求未 mock测试可能因网络波动失败 def test_fetch_weather(): result fetch_weather(Beijing) assert result[city] Beijing正确写法三层防御# ✅ 使用 mocker.patch.object 模拟 requests.Session def test_fetch_weather(mocker): # Step 1: 创建 mock 响应对象 mock_response mocker.Mock() mock_response.json.return_value {city: Beijing, temp: 25} mock_response.raise_for_status.return_value None # Step 2: patch requests.get让它返回 mock_response # 注意必须 patch requests.get不是 requests.Request mock_get mocker.patch(requests.get, return_valuemock_response) # Step 3: 执行测试 result fetch_weather(Beijing) # Step 4: 验证业务逻辑 assert result[city] Beijing # Step 5: 验证外部调用是否符合预期关键 mock_get.assert_called_once_with( https://api.weather.com/v3/weather/forecast?cityBeijing, timeout5 )实操心得我坚持在每个 mock 测试里写assert_called_once_with(...)哪怕看起来多余。因为这能捕获两类错误一是函数内部 URL 拼接逻辑错误比如漏了?city二是 timeout 参数未传递。这两类错误在真实环境中极难复现但在测试里一行断言就能揪出来。4.2 场景二模拟数据库操作ORM 层的精准控制用 SQLAlchemy 时新手常犯的错误是 mocksession.query()结果发现query.filter().all()链式调用失败。根本原因是query()返回的是Query对象而filter()是它的方法。正确解法mock Query 对象的行为# models.py from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base Base declarative_base() class User(Base): __tablename__ users id Column(Integer, primary_keyTrue) name Column(String(50))# service.py def get_active_users(session): return session.query(User).filter(User.id 0).all()# test_service.py def test_get_active_users(mocker): # Step 1: 创建 mock session mock_session mocker.Mock() # Step 2: 创建 mock query 对象并设置链式调用返回值 mock_query mocker.Mock() mock_session.query.return_value mock_query # 关键mock_query.filter() 必须返回自身才能继续调用 .all() mock_query.filter.return_value mock_query # Step 3: 设置 all() 的返回值 mock_users [User(id1, nameAlice), User(id2, nameBob)] mock_query.all.return_value mock_users # Step 4: 执行测试 result get_active_users(mock_session) # Step 5: 验证 assert len(result) 2 assert result[0].name Alice # Step 6: 验证查询条件证明 filter 被正确调用 mock_query.filter.assert_called_once() # 注意这里无法直接 assert filter(User.id 0)因为 操作符返回的是 SQL 表达式对象 # 更务实的做法是验证最终生成的 SQL见进阶技巧4.3 场景三模拟时间相关逻辑精确控制 time.sleep 和 datetimetime.sleep(300)在测试里等 5 分钟绝对不行。但mocker.patch(time.sleep)只解决延迟不解决时间戳生成。完整方案同时 mock sleep 和 datetime# cache.py import time from datetime import datetime def cache_with_timeout(data: str, timeout_sec: int 300) - str: cached_at datetime.now() time.sleep(timeout_sec) # 模拟耗时操作 return f{data} (cached at {cached_at.isoformat()})# test_cache.py def test_cache_with_timeout(mocker): # Mock time.sleep让延迟瞬间完成 mock_sleep mocker.patch(time.sleep) # Mock datetime.now固定返回特定时间 mock_now mocker.Mock() mock_now.isoformat.return_value 2023-01-01T12:00:00 mocker.patch(cache.datetime).now.return_value mock_now # 执行测试 result cache_with_timeout(test_data, 300) # 验证结果包含固定时间戳 assert 2023-01-01T12:00:00 in result # 验证 sleep 被调用一次且参数正确 mock_sleep.assert_called_once_with(300)注意事项mocker.patch(cache.datetime)中的cache是模块名不是datetime模块本身。因为cache.py里from datetime import datetime是局部导入mock 必须针对cache模块里的datetime对象。4.4 场景四模拟属性和 property绕过get方法陷阱Python 的property是描述符直接mocker.patch.object(obj, prop, return_value...)会失败因为prop不是普通属性。正确解法用 PropertyMock# user.py class User: def __init__(self, name: str, age: int): self.name name self._age age property def is_adult(self) - bool: return self._age 18 property def display_name(self) - str: return f[{self.name.upper()}]# test_user.py def test_user_properties(mocker): user User(alice, 17) # ✅ 正确用 mocker.PropertyMock 替换 property type(user).is_adult mocker.PropertyMock(return_valueTrue) type(user).display_name mocker.PropertyMock(return_value[ALICE]) # 现在可以正常访问 assert user.is_adult is True assert user.display_name [ALICE] # 验证 property 被访问注意PropertyMock 没有 assert_called需用其他方式 # 实用技巧给 PropertyMock 添加 side_effect 记录访问 access_log [] type(user).is_adult mocker.PropertyMock( return_valueTrue, side_effectlambda: access_log.append(is_adult accessed) ) _ user.is_adult assert is_adult accessed in access_log4.5 场景五高级技巧——side_effect 的 3 种实战形态side_effect是pytest-mock最强大的武器它让 mock 从“静态桩”变成“智能代理”。形态一列表式返回模拟多轮调用def test_api_retry_logic(mocker): # 模拟第一次失败第二次成功 mock_api mocker.Mock() mock_api.fetch_data.side_effect [ ConnectionError(Network timeout), {status: success, data: [1, 2, 3]} ] # 被测函数有重试逻辑 result fetch_with_retry(mock_api, max_retries2) assert result[data] [1, 2, 3] assert mock_api.fetch_data.call_count 2形态二函数式 side_effect动态生成返回值def test_dynamic_validation(mocker): # 根据输入参数返回不同结果 def validate_side_effect(username): if username admin: return True elif len(username) 3: raise ValueError(Username too short) else: return False mock_validator mocker.Mock() mock_validator.validate.side_effect validate_side_effect # 测试三种情况 assert mock_validator.validate(admin) is True with pytest.raises(ValueError, matchUsername too short): mock_validator.validate(ab) assert mock_validator.validate(user123) is False形态三异常返回混合模拟真实服务行为def test_payment_gateway(mocker): mock_gateway mocker.Mock() # 模拟前两次调用返回 success第三次抛异常第四次恢复 mock_gateway.charge.side_effect [ SUCCESS, SUCCESS, RuntimeError(Gateway overloaded), SUCCESS ] # 执行支付流程含重试 for i in range(4): try: result process_payment(mock_gateway, 100) if result SUCCESS: break except RuntimeError: continue # 验证总共调用了 4 次 assert mock_gateway.charge.call_count 44.6 场景六spy 的深度应用——验证函数调用顺序和参数结构spy不仅能验证“是否调用”还能验证“如何调用”。# logger.py import logging def log_operation(operation: str, **kwargs): logger logging.getLogger(app) logger.info(fStarting {operation}, extrakwargs) # ... 执行操作 ... logger.info(fFinished {operation}) # test_logger.py def test_log_operation(mocker): # Spy on the loggers info method mock_logger mocker.Mock() mock_info mocker.spy(mock_logger, info) # 执行被测函数 log_operation(database_backup, db_nameprod, tables[users, orders]) # 验证第一次调用Starting message extra dict first_call mock_info.call_args_list[0] assert first_call.args (Starting database_backup,) assert extra in first_call.kwargs assert first_call.kwargs[extra][db_name] prod # 验证第二次调用Finished message second_call mock_info.call_args_list[1] assert second_call.args (Finished database_backup,)4.7 场景七mocking 的反模式——什么情况下绝对不要 mock我见过最危险的 mock 是mocker.patch(myapp.models.User)。这会导致整个User类被替换成Mock对象所有User(id1)实例化都返回Mock后续user.id访问永远是Mock测试彻底失真。绝对禁止 mock 的三类对象类型反例正确做法数据模型类mocker.patch(myapp.models.User)用真实对象User(id1, nameAlice)或attrs库生成轻量对象纯函数无副作用mocker.patch(myapp.utils.calculate_tax)直接调用用pytest.mark.parametrize覆盖各种输入配置类mocker.patch(myapp.config.Settings)用pytest的monkeypatchfixture 修改Settings的类属性判断准则如果一个对象的职责是“承载数据”或“定义行为”而不是“发起 IO”就不要 mock 它。mock 的目标永远是边界API、DB、文件系统不是领域模型。5. 常见问题排查与避坑指南附真实故障记录5.1 故障一“AttributeError: Mock object has no attribute xxx” —— 90% 的 mock 新手死在这里真实案例同事写了一个函数def send_notification(user, message): user.notify(message)测试时mocker.patch.object(user, notify)报错说user没有notify属性。根因分析user是一个Mock对象而Mock默认只允许访问预设的属性。user.notify会返回一个新的Mock但user.notify(message)调用时Mock不知道notify是方法还是属性。解决方案# ✅ 正确显式声明 notify 是可调用的 user mocker.Mock() user.notify mocker.Mock(return_valuesent) # ✅ 或者更简洁用 mocker.MagicMock默认所有属性可调用 user mocker.MagicMock() user.notify.return_value sent # ✅ 最佳实践直接 mock 方法不 mock 整个对象 def test_send_notification(mocker): mock_user mocker.Mock() mock_user.notify.return_value sent # ... 测试逻辑5.2 故障二“AssertionError: Expected to be called once. Called 0 times.” —— patch 路径错了真实日志E AssertionError: Expected to be called once. Called 0 times. E Expected: mock.send_email(...) E Actual: not called.排查清单按优先级检查 patch 路径是否指向“使用位置”而非“定义位置”错误mocker.patch(myapp.email.send_email)定义在email.py正确mocker.patch(myapp.service.send_email)调用发生在service.py检查是否在 patch 之前就导入了目标模块# ❌ 错误在 patch 前导入模块已加载patch 失效 from myapp import service mocker.patch(myapp.service.send_email) # ✅ 正确在 patch 后导入或在测试函数内导入 def test_something(mocker): mocker.patch(myapp.service.send_email) from myapp import service # 延迟导入检查是否用了相对导入from .email import send_email在service.py中patch 必须写mocker.patch(myapp.service.send_email)不能写mocker.patch(myapp.email.send_email)。5.3 故障三测试通过但线上崩溃 —— mock 过度导致逻辑脱节典型症状所有单元测试 100% 通过集成测试失败报KeyError: user_id查代码发现mock 返回的字典缺少必要字段我的解决方案三步防御Schema 验证用pydantic定义响应模型在 mock 中强制返回符合 schema 的数据from pydantic import BaseModel class ApiResponse(BaseModel): status: str data: dict timestamp: str def test_api_call(mocker): mock_response mocker.Mock() # 强制返回符合 schema 的数据 mock_response.json.return_value ApiResponse( statussuccess, data{id: 1}, timestamp2023-01-01 ).dict()类型注解驱动在函数签名中写明返回类型IDE 和 mypy 会帮你 catch 类型不匹配def fetch_user(user_id: int) - dict[str, Any]: # 明确返回 dict ...回归测试兜底每周用pytest --tbno -x运行一次“最小集成测试集”只测 5 个最关键的端到端流程确保 mock 不破坏真实链路。5.4 故障四并发测试失败 —— mocker fixture 的作用域陷阱现象单个测试pytest test_file.py::test_one通过但pytest test_file.py多个测试一起跑失败报AssertionError: Expected call not found。根因mockerfixture 默认是function作用域但如果你在测试中用了mocker.patch且没用with语句patch 会持续到测试结束。如果两个测试 patch 同一个目标第二个测试会覆盖第一个。永久修复# ✅ 强制使用 with 语句确保 patch 严格限定在代码块内 def test_concurrent_safe(mocker): with mocker.patch(requests.get) as mock_get: mock_get.return_value.json.return_value {ok: True} result fetch_data() assert result[ok] # mock_get 自动 unpatch绝不会影响其他测试5.5 故障五mock 导致 IDE 调试失效 —— PyCharm 断点不触发原因PyCharm 的调试器在mocker.patch后会把被 patch 的函数识别为Mock对象断点设置在原函数上无效。解决方案在 patch 代码后加一行import pdb; pdb.set_trace()或者用mocker.stopall()临时关闭所有 mock再调试最佳实践在conftest.py中添加调试开关# conftest.py import pytest pytest.fixture(autouseTrue) def disable_mocks_for_debug(request): if request.config.getoption(--debug, defaultFalse): from pytest_mock import MockerFixture mocker MockerFixture(request.node) mocker.stopall()运行pytest --debug test_file.py即可关闭 mock 调试。6. 进阶技巧让 mock 测试更健壮、更易维护的 5 个习惯6.1 习惯一用 pytest.mark.parametrize 驱动 mock 数据而非硬编码不要这样写def test_process_payment(mocker): mock_gateway mocker.Mock() mock_gateway.charge.return_value SUCCESS assert process_payment(mock_gateway, 100) SUCCESS mock_gateway.charge.return_value FAILED assert process_payment(mock_gateway, 100) FAILED要这样写pytest.mark.parametrize(gateway_response,expected, [ (SUCCESS, SUCCESS), (FAILED, FAILED), (PENDING, PENDING), ]) def test_process_payment(mocker, gateway_response, expected): mock_gateway mocker.Mock() mock_gateway.charge.return_value gateway_response assert process_payment(mock_gateway, 100) expected好处新增测试用例只需加一行参数不用复制粘贴整个测试函数所有测试用例共享同一个 mock 结构避免遗漏assert_calledpytest 会为每个参数生成独立的测试 ID失败时精准定位6.2 习惯二为复杂 mock 创建工厂函数消除重复当 mock 逻辑超过 5 行立刻提取为工厂函数# conftest.py import pytest from unittest.mock import Mock pytest.fixture def mock_api_client(): 工厂函数返回预配置的 API 客户端 mock client Mock() client.get.return_value.status_code 200 client.get.return_value.json.return_value {} client.post.return_value.status_code 201 client.post.return_value.json.return_value {id: 1} return client # test_api.py def test_create_user(mock_api_client): result create_user(mock_api_client, Alice) assert result[id] 1 mock_api_client.post.assert_called_once()6.3 习惯三用 pytest-asyncio 测试异步 mock别再用 loop.run_until_complete对于async def fetch_data()不要这样def test_async_fetch(mocker): mock_session mocker.Mock() mock_session.get.return_value.__aenter__.return_value.json.return_value {data: 1} # ❌ 错误手动管理事件循环 import asyncio result asyncio.get_event_loop().run_until_complete( fetch_data(mock_session) )要这样import pytest pytest.mark.asyncio async def test_async_fetch(mocker): mock_session mocker.Mock() # 配置异步 mock__aenter__ 返回一个 mock其 json 是协程 mock_response mocker.Mock() mock_response.json mocker.AsyncMock(return_value{data: 1}) mock_session.get.return_value.__aenter__.return_value mock_response result await fetch_data(mock_session) assert result[data] 16.4 习惯四用 pytest-cov 验证 mock 是否覆盖了所有分支安装pytest-cov后运行pytest --covmyapp --cov-reporthtml test/打开htmlcov/index.html重点看红色行mock 未覆盖的代码比如if response.status_code 500:分支黄色行部分覆盖比如try/except中只测了try没测except这比人工检查assert_called更可靠。6.5 习惯五建立团队 mock 规范文档我们的真实模板在docs/testing-mock-policy.md中我们规定场景允许的 mock 方式禁止行为示例外部 HTTP APImocker.patch(requests.get)mockurllib3底层✅mocker.patch(requests.get)❌mocker.patch(urllib3.PoolManager.request)数据库 ORMmocker.patch.object(session, query)mocksqlalchemy.create_engine✅mocker.patch.object(session, query)❌mocker.patch(sqlalchemy.create_engine)时间相关mocker.patch(time.sleep)mocker.patch(datetime.datetime)mocktime.time()✅mocker.patch(time.sleep)❌mocker.patch(time.time)这份文档每年更新由测试负责人维护新成员入职第一周必须通读并签署确认。7. 性能对比与选型建议pytest-mock vs unittest.mock vs responses7.1 三者性能基准测试基于 1000 个 mock 测试我用pytest-benchmark实测了三种方案在

相关新闻