1. 为什么我坚持用 pytest-mock 而不是直接写 unittest.mock——一个老 Python 测试工程师的坦白你有没有过这种经历写完一个功能信心满满地跑测试结果发现 CI 环境里报错——不是代码逻辑错了而是因为测试里调用了真实数据库连接而 CI 服务器根本没配那个 PostgreSQL 实例或者更糟测试里调用了某支付网关的charge()方法结果真扣了测试账户 1 块钱还触发了风控告警邮件。我刚入行那会儿在一家做 SaaS 的创业公司就因为没搞懂 mocking连续三天被运维拉着开会就为解释“为什么测试脚本半夜自动发了 200 封邮件”。这就是为什么今天我要花整整一篇长文把pytest-mock这个插件掰开揉碎讲清楚。它不是什么高深莫测的黑科技而是一套让测试回归本职工作的务实工具验证你的函数逻辑是否正确而不是验证你能不能连上 Redis、能不能调通 OpenAI API、或者你的time.sleep(3)是不是真的睡了三秒。关键词就是三个隔离、可控、可读。pytest-mock的核心价值不在于它比unittest.mock多了什么功能事实上它底层完全复用unittest.mock而在于它彻底解决了unittest.mock在 pytest 生态里“水土不服”的问题。比如你用unittest.mock.patch得手动start()和stop()还得考虑作用域嵌套一不小心就漏了stop()导致后续测试被污染再比如patch的装饰器写法要写在函数上方和 pytest 的参数化pytest.mark.parametrize放一起时顺序稍有不慎就报错。而pytest-mock提供的mockerfixture就像一个随取随用、用完即焚的“测试沙盒”你只需要在测试函数签名里写上def test_something(mocker):它就自动帮你管理好所有 mock 对象的生命周期。这不是偷懒是把重复劳动从人脑里移除让工程师的注意力真正聚焦在“这个函数到底该有什么行为”上。我见过太多团队测试覆盖率数字很漂亮但一改代码就大面积报红。深挖下去八成是因为 mock 写得太“实”——mock 了一个类却把它的所有方法都打桩甚至 mock 返回值里还嵌套了另一个 mock 对象。这已经不是在测试你的业务逻辑而是在测试你 mock 写得对不对。pytest-mock的设计哲学恰恰是反其道而行之它鼓励你只 mock边界也就是那些真正与外部世界交互的点I/O、网络、时间、随机数而让你的业务逻辑内部保持“真实”。这样当你的calculate_discount()函数内部重构了算法只要get_discount()这个接口契约不变测试就依然绿着。这才是可持续的测试。所以如果你现在还在用from unittest.mock import patch或者更原始地自己手写Mock()对象这篇指南就是为你写的。它不会教你“如何成为 mocking 大师”而是带你建立一套稳健、可维护、能经得起代码演进考验的测试习惯。接下来的内容全部基于我过去十年在金融、电商、AI 工具等不同领域的真实项目经验每一个例子、每一条建议都踩过坑、交过学费。我们不讲虚的直接上干货。2. 项目整体设计与思路拆解从“为什么需要 mock”到“为什么是 pytest-mock”2.1 Mock 的本质不是造假而是划清责任边界很多新手对 mock 的第一印象是“造假”觉得它让测试变得不真实。这是最大的误解。Mock 的本质是在单元测试这个特定场景下主动放弃对某些组件的验证权把验证的责任明确划分出去。这就像一栋大楼的施工验收水电班组只负责检查自己铺设的管线是否通畅、压力是否达标他们不会、也不该去检查承重墙的混凝土标号——那是土建班组的事。Mock 就是水电班组给自己装上的“临时供水泵”它不模拟真实的市政水压但它能确保“只要水来了我的阀门就能正常开关”。在 Python 项目中这个“市政供水”通常指代四类东西I/O 操作文件读写open()、数据库查询cursor.execute()、网络请求requests.get()外部服务调用调用 OpenAI 的chat.completions.create()、调用 Stripe 的PaymentIntent.create()不可控状态time.time()返回的当前时间、random.random()生成的随机数、uuid.uuid4()生成的唯一 ID有副作用的操作发送邮件smtplib.SMTP.sendmail()、写日志logger.info()、修改全局配置。这些操作的共同特点是它们的执行结果不取决于你的函数逻辑而取决于外部环境。你的fetch_weather_data()函数写得再完美如果天气 API 服务器宕机了它照样返回None。单元测试的目标是验证“我的代码逻辑”而不是“外部世界是否正常”。因此我们必须把这些外部依赖“摘掉”换成一个我们完全掌控的替身。提示一个简单判断标准——如果一个函数的执行需要联网、访问磁盘、等待时间、或产生真实费用那么它在单元测试里就必须被 mock。2.2 pytest-mock 的核心优势fixture 驱动的“声明式”测试pytest-mock的最大革新是把 mock 的创建、配置、销毁这一整套流程封装成了一个 pytest fixture ——mocker。这带来了三个质的飞跃第一生命周期自动化。在unittest.mock里patch的作用域管理是个噩梦。你用patch(module.Class.method)它只在被装饰的函数内有效你用with patch(...) as mock_obj:必须确保with块完整执行否则stop()不会被调用。而mockerfixture 完全由 pytest 控制它在测试函数开始前自动start()在测试函数结束后无论成功失败自动stop()和清理。这意味着你永远不用担心 mock “泄露”到下一个测试里。我曾经在一个项目里因为一个patch忘记stop()导致后续 37 个测试全部失败排查了整整一个下午。第二API 更简洁、意图更清晰。对比一下两种写法# unittest.mock 方式冗长且易错 from unittest.mock import patch, Mock patch(requests.get) def test_fetch_data(mock_get): mock_response Mock() mock_response.status_code 200 mock_response.json.return_value {data: ok} mock_get.return_value mock_response result fetch_data() assert result {data: ok} mock_get.assert_called_once_with(https://api.example.com) # pytest-mock 方式干净利落 def test_fetch_data(mocker): mock_get mocker.patch(requests.get) mock_get.return_value.status_code 200 mock_get.return_value.json.return_value {data: ok} result fetch_data() assert result {data: ok} mock_get.assert_called_once_with(https://api.example.com)mocker.patch()直接返回一个已配置好的 mock 对象省去了Mock()的实例化步骤mock_get.return_value.status_code 200这种链式赋值比mock_response.status_code 200; mock_get.return_value mock_response少了两步中间变量代码意图一目了然我要让get()调用返回一个状态码为 200 的响应。第三与 pytest 生态无缝融合。mocker是一个真正的 pytest fixture它可以和pytest.mark.parametrize、pytest.mark.skipif、conftest.py中定义的其他 fixture如数据库连接、测试用户自由组合。你可以轻松写出这样的测试pytest.mark.parametrize(status_code,expected, [(200, success), (404, not_found)]) def test_api_handler(mocker, status_code, expected): # mock 不同的 HTTP 状态码 mock_response mocker.Mock() mock_response.status_code status_code if status_code 200: mock_response.json.return_value {result: ok} mocker.patch(myapp.api.call_external, return_valuemock_response) result handle_api_call() assert result expected这种灵活性是unittest.mock的patch装饰器无法提供的。它让测试代码的组织方式从“围绕 mock 写”变成了“围绕业务逻辑写”这才是工程化的正道。2.3 选型决策为什么不是其他方案市面上还有其他 mocking 方案比如responses专用于 HTTP mocking、pytest-asyncio用于异步测试、甚至自己手写 fake 类。但在绝大多数通用场景下pytest-mock是最优解原因如下responses它非常优秀但定位极其垂直——只解决 HTTP 请求 mocking。一旦你的代码里还有数据库、文件、时间等其他依赖你就得同时引入responses、sqlite3.connect的 mock、time.time的 patch……测试环境瞬间变得臃肿。pytest-mock是“一揽子”解决方案一个mockerfixture 解决所有问题。手写 Fake 类对于非常复杂的外部服务比如一个有十几种方法的 SDK有时确实需要一个轻量级的 Fake 实现。但这属于集成测试或端到端测试的范畴而非单元测试。单元测试追求的是“快”和“小”Fake 类的维护成本远高于一个简单的mocker.patch()。我见过一个团队为了 mock 一个云存储 SDK写了 800 行 Fake 代码结果 SDK 升级后Fake 类的接口也变了测试全挂。unittest.mock直接使用这是最“原生”的方式但正如前面所说它在 pytest 里体验割裂。pytest-mock不是替代而是增强。它没有发明新 API只是把unittest.mock的能力用 pytest 的语言重新包装了一遍。学习成本几乎为零收益却是巨大的。所以我的建议很明确把pytest-mock作为你 Python 项目测试基础设施的“默认选项”。把它加到requirements-test.txt里和pytest一起安装。只有当你遇到pytest-mock确实无法优雅解决的极端场景时比如需要深度定制 HTTP 请求的响应头、body才去考虑引入responses这样的专用库。3. 核心细节解析与实操要点从 setup 到第一个真实测试3.1 环境搭建虚拟环境 依赖安装为什么 conda 不是唯一选择原文提到了用conda创建环境这没错但作为一线工程师我必须强调venvpip是更主流、更轻量、更符合 Python 社区惯例的选择。conda在数据科学领域有优势能管理非 Python 的 C 库但对于纯 Python Web 或工具开发venv足够且更快。正确的、生产就绪的 setup 步骤应该是# 1. 创建干净的虚拟环境推荐使用 venvPython 3.3 自带 python -m venv .venv # 2. 激活环境Linux/macOS source .venv/bin/activate # 或者 Windows PowerShell # .venv\Scripts\Activate.ps1 需先执行 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser # 或者 Windows CMD # .venv\Scripts\activate.bat # 3. 升级 pip避免旧版 pip 安装包出错 pip install --upgrade pip # 4. 安装测试核心依赖 pip install pytest pytest-mock # 5. 可选但强烈推荐安装 pytest-cov 用于覆盖率报告 pip install pytest-cov注意不要用conda install pytest-mock。虽然它能装上但 conda 的包索引更新慢且可能引入不必要的依赖冲突。pip是 Python 包管理的事实标准。验证安装是否成功不能只看pytest --version。你应该运行一个最简测试确认mockerfixture 真正可用# 创建一个 test_simple.py 文件 def test_mocker_works(mocker): # 创建一个最简单的 mock mock_obj mocker.Mock() mock_obj.hello.return_value world assert mock_obj.hello() world mock_obj.hello.assert_called_once()然后在命令行运行pytest test_simple.py -v如果看到PASSED说明环境一切正常。这一步看似简单却是很多初学者卡住的第一关——他们跳过了验证直接写复杂测试结果报错fixture mocker not found却不知道是环境没装对。3.2 mocker.fixture 的三种核心用法Patch、Mock、Spymockerfixture 提供了三个最常用、最核心的方法它们覆盖了 95% 的 mocking 场景。理解它们的区别和适用场景是掌握pytest-mock的关键。1.mocker.patch()替换模块/对象/属性最常用这是你用得最多的方法用于“打补丁”把目标路径上的真实对象替换成一个 mock 对象。语法mocker.patch(target, **kwargs)target一个字符串表示你要 mock 的对象的完整导入路径。这是最容易出错的地方路径必须是你在测试代码里实际 import 并使用的路径而不是对象定义的路径。常见错误示例# ❌ 错误在 myapp/api.py 里定义了 get_user()但你在 test_api.py 里是这样 import 的 # from myapp.api import get_user # 那么 target 应该是 myapp.api.get_user而不是 myapp.api.get_user 的定义处 myapp.api.get_user # ✅ 正确假设 test_api.py 是这么写的 import myapp.api def test_get_user(): # 这里调用的是 myapp.api.get_user() result myapp.api.get_user(123) # 那么 target 就是 myapp.api.get_user实操技巧mocker.patch()返回一个 mock 对象你可以直接链式配置def test_send_email(mocker): # 一行搞定mock smtp.sendmail并让它返回 True mock_send mocker.patch(smtplib.SMTP.sendmail, return_valueTrue) result send_welcome_email(userexample.com) assert result is True # 验证它被调用了且参数正确 mock_send.assert_called_once_with( admincompany.com, [userexample.com], ANY # 使用 ANY 匹配任意内容避免因邮件正文太长而断言失败 )2.mocker.Mock()创建一个空白的 mock 对象用于构造复杂返回值当你需要 mock 一个对象并且这个对象本身有很多属性和方法需要配置时比如一个模拟的 API 响应对象mocker.Mock()就派上用场了。语法mocker.Mock(**kwargs)其中kwargs会直接设置为 mock 对象的属性。实操技巧利用return_value和side_effect构造灵活的响应def test_process_payment(mocker): # 创建一个模拟的 payment_gateway 对象 mock_gateway mocker.Mock() # 让它的 charge() 方法第一次返回 Success第二次抛出异常 mock_gateway.charge.side_effect [Success, ValueError(Card declined)] # 第一次调用 result1 process_payment(mock_gateway, 100) assert result1 Payment processed successfully # 第二次调用 with pytest.raises(ValueError, matchCard declined): process_payment(mock_gateway, 200) # 验证调用次数 assert mock_gateway.charge.call_count 23.mocker.spy()监控真实函数的调用最易被忽视的利器这是pytest-mock最被低估的功能。spy不会改变函数的行为它只是在函数外面“加了一层监控探针”记录下每一次调用的参数、返回值、调用次数。它特别适合验证“某个辅助函数是否被正确调用”而无需关心它的具体实现。语法mocker.spy(obj, method_name)实操技巧spy是调试和重构的神兵利器# 假设你有一个函数内部会调用 logger.info() def log_and_process(data): logger.info(fProcessing {len(data)} items) return process_data(data) def test_log_and_process(mocker): # spy 在 logger.info 上不干扰它的真实行为 spy_info mocker.spy(logger, info) result log_and_process([1, 2, 3]) # 断言业务逻辑 assert result [2, 4, 6] # 假设 process_data 是乘以 2 # 断言日志被正确调用 spy_info.assert_called_once_with(Processing 3 items) # 你甚至可以检查它返回了什么logger.info 通常返回 None assert spy_info.spy_return is None提示spy的最大价值在于重构安全。当你想把log_and_process()里的日志逻辑抽出来变成一个独立的log_operation()函数时你只需修改生产代码然后运行测试——如果spy_info的断言失败了说明你抽离错了立刻就能发现。3.3 关键配置项详解return_value, side_effect, new_callablepytest-mock的强大很大程度上体现在对return_value和side_effect的灵活运用上。它们是控制 mock 行为的两个核心杠杆。return_value设定固定的返回值这是最基础的用法适用于“这个函数总是返回这个值”的场景。类型可以是任何 Python 对象字符串、数字、字典、列表、甚至另一个Mock对象。实操注意当return_value是一个Mock对象时你可以继续链式配置它的属性def test_fetch_user(mocker): # mock requests.get让它返回一个 mock response mock_get mocker.patch(requests.get) mock_response mocker.Mock() mock_response.status_code 200 mock_response.json.return_value {id: 1, name: Alice} mock_get.return_value mock_response user fetch_user(1) assert user[name] Aliceside_effect模拟动态行为核心中的核心如果说return_value是静态的那么side_effect就是动态的。它让你的 mock 可以“活”起来模拟真实世界的不确定性。类型一可迭代对象list, tuple每次调用 mock就按顺序返回列表中的下一个值。这是模拟“第一次成功、第二次失败”这类场景的黄金法则。mock_func.side_effect [10, 20, 30] print(mock_func()) # 10 print(mock_func()) # 20 print(mock_func()) # 30 print(mock_func()) # 30 (循环不会抛出 StopIteration所以通常只用作有限次)类型二异常类或异常实例每次调用 mock就抛出指定的异常。这是测试错误处理路径的必备技能。mock_func.side_effect ValueError(Invalid input) # 或者 mock_func.side_effect [ValueError(First fail), success]类型三可调用对象function每次调用 mock就执行这个函数并将 mock 的参数传递给它。这是最强大的用法可以实现复杂的、基于输入的逻辑。def dynamic_response(*args, **kwargs): url args[0] if args else if success in url: return mocker.Mock(status_code200, jsonlambda: {ok: True}) else: return mocker.Mock(status_code404, jsonlambda: {error: Not found}) mock_get.side_effect dynamic_responsenew_callable指定 mock 的类型高级定制new_callable参数允许你指定 mock 对象的具体类型最常用的是mocker.MagicMock和mocker.PropertyMock。mocker.PropertyMock专门用于 mock 类的property。因为普通Mock对象的属性访问是惰性的而PropertyMock可以像真实属性一样被assert。class DatabaseConnection: property def is_connected(self): return self._connected def test_db_connection(mocker): db DatabaseConnection() # mock the property to always return True mocker.patch.object(DatabaseConnection, is_connected, new_callablemocker.PropertyMock, return_valueTrue) assert db.is_connected is True # 这行会成功mocker.MagicMock比Mock更“智能”它预定义了很多魔法方法__str__,__len__,__iter__等当你 mock 的对象需要被当作容器、字符串等使用时它能避免AttributeError。# 如果你 mock 一个 list-like 对象 mock_list mocker.MagicMock() mock_list.__len__.return_value 3 mock_list.__iter__.return_value iter([1, 2, 3]) assert len(mock_list) 3 assert list(mock_list) [1, 2, 3]4. 实操过程与核心环节实现从天气 API 到支付网关的完整链路4.1 场景一模拟外部 HTTP API天气服务让我们从最经典的例子开始一个获取天气数据的函数。它依赖requests.get而我们绝不想在测试时真的发请求。生产代码 (weather.py)import requests import logging logger logging.getLogger(__name__) def fetch_weather_data(city: str, api_key: str) - dict: Fetch current weather data for a city from external API. Returns a dict like {temperature: 22.5, condition: Sunny} or None on error. url fhttps://api.weatherapi.com/v1/current.json?key{api_key}q{city} try: logger.info(fCalling weather API for {city}) response requests.get(url, timeout5) response.raise_for_status() # Raises an HTTPError for bad responses data response.json() return { temperature: data[current][temp_c], condition: data[current][condition][text] } except (requests.RequestException, KeyError, ValueError) as e: logger.error(fWeather API call failed for {city}: {e}) return None测试代码 (test_weather.py)import pytest from weather import fetch_weather_data def test_fetch_weather_success(mocker): Test successful weather API call. # 1. Mock the requests.get call mock_get mocker.patch(weather.requests.get) # 2. Create a realistic mock response object mock_response mocker.Mock() mock_response.status_code 200 mock_response.raise_for_status.return_value None # No exception # 3. Configure the JSON data that .json() will return mock_response.json.return_value { current: { temp_c: 22.5, condition: {text: Sunny} } } # 4. Make mock_get return our mock_response mock_get.return_value mock_response # 5. Call the function under test result fetch_weather_data(London, fake-api-key) # 6. Assert the business logic output assert result {temperature: 22.5, condition: Sunny} # 7. Assert the external call was made correctly mock_get.assert_called_once_with( https://api.weatherapi.com/v1/current.json?keyfake-api-keyqLondon, timeout5 ) def test_fetch_weather_failure(mocker): Test weather API call failure (network error). mock_get mocker.patch(weather.requests.get) # Simulate a network timeout mock_get.side_effect requests.Timeout(Request timed out) result fetch_weather_data(London, fake-api-key) assert result is None def test_fetch_weather_bad_response(mocker): Test weather API returns invalid JSON. mock_get mocker.patch(weather.requests.get) mock_response mocker.Mock() mock_response.status_code 200 mock_response.raise_for_status.return_value None # Make .json() raise a ValueError mock_response.json.side_effect ValueError(Invalid JSON) result fetch_weather_data(London, fake-api-key) assert result is None实操心得我刻意在test_fetch_weather_success里把mock_response.json.return_value设为一个结构完全匹配真实 API 响应的字典。这不是多此一举而是为了保证你的解析逻辑data[current][temp_c]在测试里也能走通。如果 mock 的结构太“扁平”比如直接{temp_c: 22.5}那么生产代码里的data[current][temp_c]就会抛KeyError测试就失去了意义。test_fetch_weather_failure和test_fetch_weather_bad_response展示了side_effect的威力。它们分别模拟了网络层和解析层的失败确保你的try...except块被充分测试。一个健壮的函数其错误处理路径的测试覆盖率应该不低于主逻辑。4.2 场景二模拟数据库操作SQLAlchemy数据库是另一个高频 mock 对象。我们以一个简单的用户查询为例。生产代码 (database.py)from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker engine create_engine(sqlite:///app.db) SessionLocal sessionmaker(autocommitFalse, autoflushFalse, bindengine) def get_user_by_id(user_id: int) - dict: Get user by ID from database. db SessionLocal() try: # Using raw SQL for simplicity, but same applies to ORM queries result db.execute(text(SELECT id, name, email FROM users WHERE id :id), {id: user_id}) row result.fetchone() if row: return {id: row[0], name: row[1], email: row[2]} return None finally: db.close()测试代码 (test_database.py)import pytest from unittest.mock import ANY from database import get_user_by_id def test_get_user_by_id_found(mocker): Test getting a user that exists. # Mock the entire SessionLocal() call to return a mock session mock_session_class mocker.patch(database.SessionLocal) mock_session mocker.Mock() mock_session_class.return_value mock_session # Mock the execute method to return a mock result mock_result mocker.Mock() mock_result.fetchone.return_value (1, Alice, aliceexample.com) mock_session.execute.return_value mock_result result get_user_by_id(1) assert result {id: 1, name: Alice, email: aliceexample.com} # Verify the SQL was executed with correct parameter mock_session.execute.assert_called_once() # We cant easily assert the exact SQL string because of text() wrapper, # so we assert it was called with *something* containing the user_id # This is where ANY comes in handy mock_session.execute.assert_called_once_with(ANY, {id: 1}) def test_get_user_by_id_not_found(mocker): Test getting a user that does not exist. mock_session_class mocker.patch(database.SessionLocal) mock_session mocker.Mock() mock_session_class.return_value mock_session mock_result mocker.Mock() mock_result.fetchone.return_value None # No row found mock_session.execute.return_value mock_result result get_user_by_id(999) assert result is None实操心得这里我们 mock 的是SessionLocal()这个工厂函数而不是session.execute()。因为SessionLocal()是我们代码里实际调用的入口点。如果去 mocksqlalchemy.orm.session.Session.execute那路径就错了。注意mock_session.execute.assert_called_once_with(ANY, {id: 1})这一行。ANY是unittest.mock提供的一个特殊对象它匹配任何值。我们用它来忽略text(...)对象的具体内容只关心传入的参数字典{id: 1}是否正确。这是编写稳定、不脆弱测试的关键技巧——只断言你真正关心的东西。4.3 场景三模拟时间与随机性支付风控最后我们来看一个更“刁钻”的场景一个支付风控函数它根据当前时间和用户历史行为决定是否放行交易。生产代码 (payment_risk.py)import time import random from datetime import datetime def should_allow_payment(user_id: int, amount: float) - bool: Simple risk check: allow if not too much money in last hour, and not a suspicious time. now datetime.now() # Block payments between 2am and 4am if 2 now.hour 4: return False # Simulate checking users recent transaction history (this would be a DB call) # For demo, well just use a random chance based on amount if amount 1000: # High value: 90% chance of being flagged return random.random() 0.1 else: # Low value: 95% chance of being allowed return random.random() 0.95测试代码 (test_payment_risk.py)import pytest from datetime import datetime from payment_risk import should_allow_payment def test_should_allow_payment_daytime(mocker): Test payment allowed during daytime. # Spy on datetime.now to control the current time mock_now mocker.patch(payment_risk.datetime) mock_now.now.return_value datetime(2023, 10, 1, 14, 30, 0) # 2:30 PM # Mock random.random to return a fixed value mocker.patch(payment_risk.random.random, return_value0.05) # 0.1, so high amount is allowed result should_allow_payment(123, 1500) assert result is True def test_should_allow_payment_nighttime_blocked(mocker): Test payment blocked during nighttime (2-4am). mock_now mocker.patch(payment_risk.datetime) mock_now.now.return_value datetime(2023, 10, 1, 3, 0, 0) # 3:00 AM # Even if random says allow, the time check should block it mocker.patch(payment_risk.random.random, return_value0.01) result should_allow_payment(123, 500) assert result is False def test_should_allow_payment_low_amount(mocker): Test low amount is usually allowed. mock_now mocker.patch(payment_risk.datetime) mock_now.now.return_value datetime(2023, 10, 1, 10, 0, 0) # 10:00 AM # Mock random to return a value that makes low amount pass mocker.patch(payment_risk.random.random, return_value0.5) # 0.5 0.95, so allowed result should_allow_payment(123, 500) assert result is True实操心得这里我们用了两个patch一个patch(payment_risk.datetime)来控制时间一个patch(payment_risk.random.random)来控制随机性。这是处理“不可控外部状态”的标准范式。注意mock_now.now.return_value的写法。datetime是一个类now()是它的类方法。所以我们需要 mockdatetime.now并让它返回一个我们构造的datetime实例。不能写成mocker.patch(datetime.datetime.now)因为datetime模块本身没有datetime子模块路径是错的。这个例子完美诠释了 mock 的目的我们不是在测试 Python 的datetime模块或random模块是否工作而是在测试should_allow_payment()这个函数的业务规则逻辑。通过固定时间和随机种子我们让测试变得完全确定、可重复。5. 常见问题与排查技巧实录那些年我踩过的坑5.1 经典报错与速查表报错信息原因分析解决方案我的血泪教训fixture mocker not foundpytest-mock未安装或安装在错误的 Python 环境中运行which python和which pip确认环境一致执行pip install pytest-mock我曾在一个项目里pyenv切换了 Python 版本但忘了pip install结果在终端里pip list看不到pytest-mock却在 IDE 里能看到因为 IDE 用了另一个 interpreter。TypeError: MagicMock ... is not JSON serializable你把一个 mock 对象如mock_response
pytest-mock实战指南:用mocker fixture实现隔离可控的Python单元测试
发布时间:2026/5/26 8:26:20
1. 为什么我坚持用 pytest-mock 而不是直接写 unittest.mock——一个老 Python 测试工程师的坦白你有没有过这种经历写完一个功能信心满满地跑测试结果发现 CI 环境里报错——不是代码逻辑错了而是因为测试里调用了真实数据库连接而 CI 服务器根本没配那个 PostgreSQL 实例或者更糟测试里调用了某支付网关的charge()方法结果真扣了测试账户 1 块钱还触发了风控告警邮件。我刚入行那会儿在一家做 SaaS 的创业公司就因为没搞懂 mocking连续三天被运维拉着开会就为解释“为什么测试脚本半夜自动发了 200 封邮件”。这就是为什么今天我要花整整一篇长文把pytest-mock这个插件掰开揉碎讲清楚。它不是什么高深莫测的黑科技而是一套让测试回归本职工作的务实工具验证你的函数逻辑是否正确而不是验证你能不能连上 Redis、能不能调通 OpenAI API、或者你的time.sleep(3)是不是真的睡了三秒。关键词就是三个隔离、可控、可读。pytest-mock的核心价值不在于它比unittest.mock多了什么功能事实上它底层完全复用unittest.mock而在于它彻底解决了unittest.mock在 pytest 生态里“水土不服”的问题。比如你用unittest.mock.patch得手动start()和stop()还得考虑作用域嵌套一不小心就漏了stop()导致后续测试被污染再比如patch的装饰器写法要写在函数上方和 pytest 的参数化pytest.mark.parametrize放一起时顺序稍有不慎就报错。而pytest-mock提供的mockerfixture就像一个随取随用、用完即焚的“测试沙盒”你只需要在测试函数签名里写上def test_something(mocker):它就自动帮你管理好所有 mock 对象的生命周期。这不是偷懒是把重复劳动从人脑里移除让工程师的注意力真正聚焦在“这个函数到底该有什么行为”上。我见过太多团队测试覆盖率数字很漂亮但一改代码就大面积报红。深挖下去八成是因为 mock 写得太“实”——mock 了一个类却把它的所有方法都打桩甚至 mock 返回值里还嵌套了另一个 mock 对象。这已经不是在测试你的业务逻辑而是在测试你 mock 写得对不对。pytest-mock的设计哲学恰恰是反其道而行之它鼓励你只 mock边界也就是那些真正与外部世界交互的点I/O、网络、时间、随机数而让你的业务逻辑内部保持“真实”。这样当你的calculate_discount()函数内部重构了算法只要get_discount()这个接口契约不变测试就依然绿着。这才是可持续的测试。所以如果你现在还在用from unittest.mock import patch或者更原始地自己手写Mock()对象这篇指南就是为你写的。它不会教你“如何成为 mocking 大师”而是带你建立一套稳健、可维护、能经得起代码演进考验的测试习惯。接下来的内容全部基于我过去十年在金融、电商、AI 工具等不同领域的真实项目经验每一个例子、每一条建议都踩过坑、交过学费。我们不讲虚的直接上干货。2. 项目整体设计与思路拆解从“为什么需要 mock”到“为什么是 pytest-mock”2.1 Mock 的本质不是造假而是划清责任边界很多新手对 mock 的第一印象是“造假”觉得它让测试变得不真实。这是最大的误解。Mock 的本质是在单元测试这个特定场景下主动放弃对某些组件的验证权把验证的责任明确划分出去。这就像一栋大楼的施工验收水电班组只负责检查自己铺设的管线是否通畅、压力是否达标他们不会、也不该去检查承重墙的混凝土标号——那是土建班组的事。Mock 就是水电班组给自己装上的“临时供水泵”它不模拟真实的市政水压但它能确保“只要水来了我的阀门就能正常开关”。在 Python 项目中这个“市政供水”通常指代四类东西I/O 操作文件读写open()、数据库查询cursor.execute()、网络请求requests.get()外部服务调用调用 OpenAI 的chat.completions.create()、调用 Stripe 的PaymentIntent.create()不可控状态time.time()返回的当前时间、random.random()生成的随机数、uuid.uuid4()生成的唯一 ID有副作用的操作发送邮件smtplib.SMTP.sendmail()、写日志logger.info()、修改全局配置。这些操作的共同特点是它们的执行结果不取决于你的函数逻辑而取决于外部环境。你的fetch_weather_data()函数写得再完美如果天气 API 服务器宕机了它照样返回None。单元测试的目标是验证“我的代码逻辑”而不是“外部世界是否正常”。因此我们必须把这些外部依赖“摘掉”换成一个我们完全掌控的替身。提示一个简单判断标准——如果一个函数的执行需要联网、访问磁盘、等待时间、或产生真实费用那么它在单元测试里就必须被 mock。2.2 pytest-mock 的核心优势fixture 驱动的“声明式”测试pytest-mock的最大革新是把 mock 的创建、配置、销毁这一整套流程封装成了一个 pytest fixture ——mocker。这带来了三个质的飞跃第一生命周期自动化。在unittest.mock里patch的作用域管理是个噩梦。你用patch(module.Class.method)它只在被装饰的函数内有效你用with patch(...) as mock_obj:必须确保with块完整执行否则stop()不会被调用。而mockerfixture 完全由 pytest 控制它在测试函数开始前自动start()在测试函数结束后无论成功失败自动stop()和清理。这意味着你永远不用担心 mock “泄露”到下一个测试里。我曾经在一个项目里因为一个patch忘记stop()导致后续 37 个测试全部失败排查了整整一个下午。第二API 更简洁、意图更清晰。对比一下两种写法# unittest.mock 方式冗长且易错 from unittest.mock import patch, Mock patch(requests.get) def test_fetch_data(mock_get): mock_response Mock() mock_response.status_code 200 mock_response.json.return_value {data: ok} mock_get.return_value mock_response result fetch_data() assert result {data: ok} mock_get.assert_called_once_with(https://api.example.com) # pytest-mock 方式干净利落 def test_fetch_data(mocker): mock_get mocker.patch(requests.get) mock_get.return_value.status_code 200 mock_get.return_value.json.return_value {data: ok} result fetch_data() assert result {data: ok} mock_get.assert_called_once_with(https://api.example.com)mocker.patch()直接返回一个已配置好的 mock 对象省去了Mock()的实例化步骤mock_get.return_value.status_code 200这种链式赋值比mock_response.status_code 200; mock_get.return_value mock_response少了两步中间变量代码意图一目了然我要让get()调用返回一个状态码为 200 的响应。第三与 pytest 生态无缝融合。mocker是一个真正的 pytest fixture它可以和pytest.mark.parametrize、pytest.mark.skipif、conftest.py中定义的其他 fixture如数据库连接、测试用户自由组合。你可以轻松写出这样的测试pytest.mark.parametrize(status_code,expected, [(200, success), (404, not_found)]) def test_api_handler(mocker, status_code, expected): # mock 不同的 HTTP 状态码 mock_response mocker.Mock() mock_response.status_code status_code if status_code 200: mock_response.json.return_value {result: ok} mocker.patch(myapp.api.call_external, return_valuemock_response) result handle_api_call() assert result expected这种灵活性是unittest.mock的patch装饰器无法提供的。它让测试代码的组织方式从“围绕 mock 写”变成了“围绕业务逻辑写”这才是工程化的正道。2.3 选型决策为什么不是其他方案市面上还有其他 mocking 方案比如responses专用于 HTTP mocking、pytest-asyncio用于异步测试、甚至自己手写 fake 类。但在绝大多数通用场景下pytest-mock是最优解原因如下responses它非常优秀但定位极其垂直——只解决 HTTP 请求 mocking。一旦你的代码里还有数据库、文件、时间等其他依赖你就得同时引入responses、sqlite3.connect的 mock、time.time的 patch……测试环境瞬间变得臃肿。pytest-mock是“一揽子”解决方案一个mockerfixture 解决所有问题。手写 Fake 类对于非常复杂的外部服务比如一个有十几种方法的 SDK有时确实需要一个轻量级的 Fake 实现。但这属于集成测试或端到端测试的范畴而非单元测试。单元测试追求的是“快”和“小”Fake 类的维护成本远高于一个简单的mocker.patch()。我见过一个团队为了 mock 一个云存储 SDK写了 800 行 Fake 代码结果 SDK 升级后Fake 类的接口也变了测试全挂。unittest.mock直接使用这是最“原生”的方式但正如前面所说它在 pytest 里体验割裂。pytest-mock不是替代而是增强。它没有发明新 API只是把unittest.mock的能力用 pytest 的语言重新包装了一遍。学习成本几乎为零收益却是巨大的。所以我的建议很明确把pytest-mock作为你 Python 项目测试基础设施的“默认选项”。把它加到requirements-test.txt里和pytest一起安装。只有当你遇到pytest-mock确实无法优雅解决的极端场景时比如需要深度定制 HTTP 请求的响应头、body才去考虑引入responses这样的专用库。3. 核心细节解析与实操要点从 setup 到第一个真实测试3.1 环境搭建虚拟环境 依赖安装为什么 conda 不是唯一选择原文提到了用conda创建环境这没错但作为一线工程师我必须强调venvpip是更主流、更轻量、更符合 Python 社区惯例的选择。conda在数据科学领域有优势能管理非 Python 的 C 库但对于纯 Python Web 或工具开发venv足够且更快。正确的、生产就绪的 setup 步骤应该是# 1. 创建干净的虚拟环境推荐使用 venvPython 3.3 自带 python -m venv .venv # 2. 激活环境Linux/macOS source .venv/bin/activate # 或者 Windows PowerShell # .venv\Scripts\Activate.ps1 需先执行 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser # 或者 Windows CMD # .venv\Scripts\activate.bat # 3. 升级 pip避免旧版 pip 安装包出错 pip install --upgrade pip # 4. 安装测试核心依赖 pip install pytest pytest-mock # 5. 可选但强烈推荐安装 pytest-cov 用于覆盖率报告 pip install pytest-cov注意不要用conda install pytest-mock。虽然它能装上但 conda 的包索引更新慢且可能引入不必要的依赖冲突。pip是 Python 包管理的事实标准。验证安装是否成功不能只看pytest --version。你应该运行一个最简测试确认mockerfixture 真正可用# 创建一个 test_simple.py 文件 def test_mocker_works(mocker): # 创建一个最简单的 mock mock_obj mocker.Mock() mock_obj.hello.return_value world assert mock_obj.hello() world mock_obj.hello.assert_called_once()然后在命令行运行pytest test_simple.py -v如果看到PASSED说明环境一切正常。这一步看似简单却是很多初学者卡住的第一关——他们跳过了验证直接写复杂测试结果报错fixture mocker not found却不知道是环境没装对。3.2 mocker.fixture 的三种核心用法Patch、Mock、Spymockerfixture 提供了三个最常用、最核心的方法它们覆盖了 95% 的 mocking 场景。理解它们的区别和适用场景是掌握pytest-mock的关键。1.mocker.patch()替换模块/对象/属性最常用这是你用得最多的方法用于“打补丁”把目标路径上的真实对象替换成一个 mock 对象。语法mocker.patch(target, **kwargs)target一个字符串表示你要 mock 的对象的完整导入路径。这是最容易出错的地方路径必须是你在测试代码里实际 import 并使用的路径而不是对象定义的路径。常见错误示例# ❌ 错误在 myapp/api.py 里定义了 get_user()但你在 test_api.py 里是这样 import 的 # from myapp.api import get_user # 那么 target 应该是 myapp.api.get_user而不是 myapp.api.get_user 的定义处 myapp.api.get_user # ✅ 正确假设 test_api.py 是这么写的 import myapp.api def test_get_user(): # 这里调用的是 myapp.api.get_user() result myapp.api.get_user(123) # 那么 target 就是 myapp.api.get_user实操技巧mocker.patch()返回一个 mock 对象你可以直接链式配置def test_send_email(mocker): # 一行搞定mock smtp.sendmail并让它返回 True mock_send mocker.patch(smtplib.SMTP.sendmail, return_valueTrue) result send_welcome_email(userexample.com) assert result is True # 验证它被调用了且参数正确 mock_send.assert_called_once_with( admincompany.com, [userexample.com], ANY # 使用 ANY 匹配任意内容避免因邮件正文太长而断言失败 )2.mocker.Mock()创建一个空白的 mock 对象用于构造复杂返回值当你需要 mock 一个对象并且这个对象本身有很多属性和方法需要配置时比如一个模拟的 API 响应对象mocker.Mock()就派上用场了。语法mocker.Mock(**kwargs)其中kwargs会直接设置为 mock 对象的属性。实操技巧利用return_value和side_effect构造灵活的响应def test_process_payment(mocker): # 创建一个模拟的 payment_gateway 对象 mock_gateway mocker.Mock() # 让它的 charge() 方法第一次返回 Success第二次抛出异常 mock_gateway.charge.side_effect [Success, ValueError(Card declined)] # 第一次调用 result1 process_payment(mock_gateway, 100) assert result1 Payment processed successfully # 第二次调用 with pytest.raises(ValueError, matchCard declined): process_payment(mock_gateway, 200) # 验证调用次数 assert mock_gateway.charge.call_count 23.mocker.spy()监控真实函数的调用最易被忽视的利器这是pytest-mock最被低估的功能。spy不会改变函数的行为它只是在函数外面“加了一层监控探针”记录下每一次调用的参数、返回值、调用次数。它特别适合验证“某个辅助函数是否被正确调用”而无需关心它的具体实现。语法mocker.spy(obj, method_name)实操技巧spy是调试和重构的神兵利器# 假设你有一个函数内部会调用 logger.info() def log_and_process(data): logger.info(fProcessing {len(data)} items) return process_data(data) def test_log_and_process(mocker): # spy 在 logger.info 上不干扰它的真实行为 spy_info mocker.spy(logger, info) result log_and_process([1, 2, 3]) # 断言业务逻辑 assert result [2, 4, 6] # 假设 process_data 是乘以 2 # 断言日志被正确调用 spy_info.assert_called_once_with(Processing 3 items) # 你甚至可以检查它返回了什么logger.info 通常返回 None assert spy_info.spy_return is None提示spy的最大价值在于重构安全。当你想把log_and_process()里的日志逻辑抽出来变成一个独立的log_operation()函数时你只需修改生产代码然后运行测试——如果spy_info的断言失败了说明你抽离错了立刻就能发现。3.3 关键配置项详解return_value, side_effect, new_callablepytest-mock的强大很大程度上体现在对return_value和side_effect的灵活运用上。它们是控制 mock 行为的两个核心杠杆。return_value设定固定的返回值这是最基础的用法适用于“这个函数总是返回这个值”的场景。类型可以是任何 Python 对象字符串、数字、字典、列表、甚至另一个Mock对象。实操注意当return_value是一个Mock对象时你可以继续链式配置它的属性def test_fetch_user(mocker): # mock requests.get让它返回一个 mock response mock_get mocker.patch(requests.get) mock_response mocker.Mock() mock_response.status_code 200 mock_response.json.return_value {id: 1, name: Alice} mock_get.return_value mock_response user fetch_user(1) assert user[name] Aliceside_effect模拟动态行为核心中的核心如果说return_value是静态的那么side_effect就是动态的。它让你的 mock 可以“活”起来模拟真实世界的不确定性。类型一可迭代对象list, tuple每次调用 mock就按顺序返回列表中的下一个值。这是模拟“第一次成功、第二次失败”这类场景的黄金法则。mock_func.side_effect [10, 20, 30] print(mock_func()) # 10 print(mock_func()) # 20 print(mock_func()) # 30 print(mock_func()) # 30 (循环不会抛出 StopIteration所以通常只用作有限次)类型二异常类或异常实例每次调用 mock就抛出指定的异常。这是测试错误处理路径的必备技能。mock_func.side_effect ValueError(Invalid input) # 或者 mock_func.side_effect [ValueError(First fail), success]类型三可调用对象function每次调用 mock就执行这个函数并将 mock 的参数传递给它。这是最强大的用法可以实现复杂的、基于输入的逻辑。def dynamic_response(*args, **kwargs): url args[0] if args else if success in url: return mocker.Mock(status_code200, jsonlambda: {ok: True}) else: return mocker.Mock(status_code404, jsonlambda: {error: Not found}) mock_get.side_effect dynamic_responsenew_callable指定 mock 的类型高级定制new_callable参数允许你指定 mock 对象的具体类型最常用的是mocker.MagicMock和mocker.PropertyMock。mocker.PropertyMock专门用于 mock 类的property。因为普通Mock对象的属性访问是惰性的而PropertyMock可以像真实属性一样被assert。class DatabaseConnection: property def is_connected(self): return self._connected def test_db_connection(mocker): db DatabaseConnection() # mock the property to always return True mocker.patch.object(DatabaseConnection, is_connected, new_callablemocker.PropertyMock, return_valueTrue) assert db.is_connected is True # 这行会成功mocker.MagicMock比Mock更“智能”它预定义了很多魔法方法__str__,__len__,__iter__等当你 mock 的对象需要被当作容器、字符串等使用时它能避免AttributeError。# 如果你 mock 一个 list-like 对象 mock_list mocker.MagicMock() mock_list.__len__.return_value 3 mock_list.__iter__.return_value iter([1, 2, 3]) assert len(mock_list) 3 assert list(mock_list) [1, 2, 3]4. 实操过程与核心环节实现从天气 API 到支付网关的完整链路4.1 场景一模拟外部 HTTP API天气服务让我们从最经典的例子开始一个获取天气数据的函数。它依赖requests.get而我们绝不想在测试时真的发请求。生产代码 (weather.py)import requests import logging logger logging.getLogger(__name__) def fetch_weather_data(city: str, api_key: str) - dict: Fetch current weather data for a city from external API. Returns a dict like {temperature: 22.5, condition: Sunny} or None on error. url fhttps://api.weatherapi.com/v1/current.json?key{api_key}q{city} try: logger.info(fCalling weather API for {city}) response requests.get(url, timeout5) response.raise_for_status() # Raises an HTTPError for bad responses data response.json() return { temperature: data[current][temp_c], condition: data[current][condition][text] } except (requests.RequestException, KeyError, ValueError) as e: logger.error(fWeather API call failed for {city}: {e}) return None测试代码 (test_weather.py)import pytest from weather import fetch_weather_data def test_fetch_weather_success(mocker): Test successful weather API call. # 1. Mock the requests.get call mock_get mocker.patch(weather.requests.get) # 2. Create a realistic mock response object mock_response mocker.Mock() mock_response.status_code 200 mock_response.raise_for_status.return_value None # No exception # 3. Configure the JSON data that .json() will return mock_response.json.return_value { current: { temp_c: 22.5, condition: {text: Sunny} } } # 4. Make mock_get return our mock_response mock_get.return_value mock_response # 5. Call the function under test result fetch_weather_data(London, fake-api-key) # 6. Assert the business logic output assert result {temperature: 22.5, condition: Sunny} # 7. Assert the external call was made correctly mock_get.assert_called_once_with( https://api.weatherapi.com/v1/current.json?keyfake-api-keyqLondon, timeout5 ) def test_fetch_weather_failure(mocker): Test weather API call failure (network error). mock_get mocker.patch(weather.requests.get) # Simulate a network timeout mock_get.side_effect requests.Timeout(Request timed out) result fetch_weather_data(London, fake-api-key) assert result is None def test_fetch_weather_bad_response(mocker): Test weather API returns invalid JSON. mock_get mocker.patch(weather.requests.get) mock_response mocker.Mock() mock_response.status_code 200 mock_response.raise_for_status.return_value None # Make .json() raise a ValueError mock_response.json.side_effect ValueError(Invalid JSON) result fetch_weather_data(London, fake-api-key) assert result is None实操心得我刻意在test_fetch_weather_success里把mock_response.json.return_value设为一个结构完全匹配真实 API 响应的字典。这不是多此一举而是为了保证你的解析逻辑data[current][temp_c]在测试里也能走通。如果 mock 的结构太“扁平”比如直接{temp_c: 22.5}那么生产代码里的data[current][temp_c]就会抛KeyError测试就失去了意义。test_fetch_weather_failure和test_fetch_weather_bad_response展示了side_effect的威力。它们分别模拟了网络层和解析层的失败确保你的try...except块被充分测试。一个健壮的函数其错误处理路径的测试覆盖率应该不低于主逻辑。4.2 场景二模拟数据库操作SQLAlchemy数据库是另一个高频 mock 对象。我们以一个简单的用户查询为例。生产代码 (database.py)from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker engine create_engine(sqlite:///app.db) SessionLocal sessionmaker(autocommitFalse, autoflushFalse, bindengine) def get_user_by_id(user_id: int) - dict: Get user by ID from database. db SessionLocal() try: # Using raw SQL for simplicity, but same applies to ORM queries result db.execute(text(SELECT id, name, email FROM users WHERE id :id), {id: user_id}) row result.fetchone() if row: return {id: row[0], name: row[1], email: row[2]} return None finally: db.close()测试代码 (test_database.py)import pytest from unittest.mock import ANY from database import get_user_by_id def test_get_user_by_id_found(mocker): Test getting a user that exists. # Mock the entire SessionLocal() call to return a mock session mock_session_class mocker.patch(database.SessionLocal) mock_session mocker.Mock() mock_session_class.return_value mock_session # Mock the execute method to return a mock result mock_result mocker.Mock() mock_result.fetchone.return_value (1, Alice, aliceexample.com) mock_session.execute.return_value mock_result result get_user_by_id(1) assert result {id: 1, name: Alice, email: aliceexample.com} # Verify the SQL was executed with correct parameter mock_session.execute.assert_called_once() # We cant easily assert the exact SQL string because of text() wrapper, # so we assert it was called with *something* containing the user_id # This is where ANY comes in handy mock_session.execute.assert_called_once_with(ANY, {id: 1}) def test_get_user_by_id_not_found(mocker): Test getting a user that does not exist. mock_session_class mocker.patch(database.SessionLocal) mock_session mocker.Mock() mock_session_class.return_value mock_session mock_result mocker.Mock() mock_result.fetchone.return_value None # No row found mock_session.execute.return_value mock_result result get_user_by_id(999) assert result is None实操心得这里我们 mock 的是SessionLocal()这个工厂函数而不是session.execute()。因为SessionLocal()是我们代码里实际调用的入口点。如果去 mocksqlalchemy.orm.session.Session.execute那路径就错了。注意mock_session.execute.assert_called_once_with(ANY, {id: 1})这一行。ANY是unittest.mock提供的一个特殊对象它匹配任何值。我们用它来忽略text(...)对象的具体内容只关心传入的参数字典{id: 1}是否正确。这是编写稳定、不脆弱测试的关键技巧——只断言你真正关心的东西。4.3 场景三模拟时间与随机性支付风控最后我们来看一个更“刁钻”的场景一个支付风控函数它根据当前时间和用户历史行为决定是否放行交易。生产代码 (payment_risk.py)import time import random from datetime import datetime def should_allow_payment(user_id: int, amount: float) - bool: Simple risk check: allow if not too much money in last hour, and not a suspicious time. now datetime.now() # Block payments between 2am and 4am if 2 now.hour 4: return False # Simulate checking users recent transaction history (this would be a DB call) # For demo, well just use a random chance based on amount if amount 1000: # High value: 90% chance of being flagged return random.random() 0.1 else: # Low value: 95% chance of being allowed return random.random() 0.95测试代码 (test_payment_risk.py)import pytest from datetime import datetime from payment_risk import should_allow_payment def test_should_allow_payment_daytime(mocker): Test payment allowed during daytime. # Spy on datetime.now to control the current time mock_now mocker.patch(payment_risk.datetime) mock_now.now.return_value datetime(2023, 10, 1, 14, 30, 0) # 2:30 PM # Mock random.random to return a fixed value mocker.patch(payment_risk.random.random, return_value0.05) # 0.1, so high amount is allowed result should_allow_payment(123, 1500) assert result is True def test_should_allow_payment_nighttime_blocked(mocker): Test payment blocked during nighttime (2-4am). mock_now mocker.patch(payment_risk.datetime) mock_now.now.return_value datetime(2023, 10, 1, 3, 0, 0) # 3:00 AM # Even if random says allow, the time check should block it mocker.patch(payment_risk.random.random, return_value0.01) result should_allow_payment(123, 500) assert result is False def test_should_allow_payment_low_amount(mocker): Test low amount is usually allowed. mock_now mocker.patch(payment_risk.datetime) mock_now.now.return_value datetime(2023, 10, 1, 10, 0, 0) # 10:00 AM # Mock random to return a value that makes low amount pass mocker.patch(payment_risk.random.random, return_value0.5) # 0.5 0.95, so allowed result should_allow_payment(123, 500) assert result is True实操心得这里我们用了两个patch一个patch(payment_risk.datetime)来控制时间一个patch(payment_risk.random.random)来控制随机性。这是处理“不可控外部状态”的标准范式。注意mock_now.now.return_value的写法。datetime是一个类now()是它的类方法。所以我们需要 mockdatetime.now并让它返回一个我们构造的datetime实例。不能写成mocker.patch(datetime.datetime.now)因为datetime模块本身没有datetime子模块路径是错的。这个例子完美诠释了 mock 的目的我们不是在测试 Python 的datetime模块或random模块是否工作而是在测试should_allow_payment()这个函数的业务规则逻辑。通过固定时间和随机种子我们让测试变得完全确定、可重复。5. 常见问题与排查技巧实录那些年我踩过的坑5.1 经典报错与速查表报错信息原因分析解决方案我的血泪教训fixture mocker not foundpytest-mock未安装或安装在错误的 Python 环境中运行which python和which pip确认环境一致执行pip install pytest-mock我曾在一个项目里pyenv切换了 Python 版本但忘了pip install结果在终端里pip list看不到pytest-mock却在 IDE 里能看到因为 IDE 用了另一个 interpreter。TypeError: MagicMock ... is not JSON serializable你把一个 mock 对象如mock_response