十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pytest Mock实战精讲:搞定接口自动化与复杂依赖场景

Pytest Mock实战精讲:搞定接口自动化与复杂依赖场景 做测试开发这几年我用 Pytest 的时间占了一大半而 Mock 又是这里面最容易被轻视、实际坑最多的东西。很多人对 Mock 的印象就停留在“把接口返回结果改成一个假数据”直到某天被测函数调了三次外部服务、前两次抛异常第三次才成功或者要 mock 的其实是个异步上下文管理器才发现手里的那几招完全不够用。这篇文章我不想讲那种“Mock 入门第一课”而是把我在真实项目里反复用到的技巧、踩过的坑、以及为什么这样写才可靠的思路一次性整理清楚。适合已经在用 Pytest 写接口自动化或单元测试但遇到复杂依赖场景时不知道怎么下手的同学也适合刚学完 Mock 基础语法想知道“我到底该怎么组织代码”的新手。1. 整体思路拆解为什么你的测试离不开 Mock1.1 先搞清楚 Mock 到底解决了什么问题很多人对 Mock 的第一反应是“造假数据”这个理解不能说错但太窄了。Mock 真正解决的是测试里的不可控依赖问题。你写接口自动化上游对接的是支付网关、短信通道、第三方开放平台这些服务在你本地测试环境根本调不通你写单元测试被测函数要查数据库、读 Redis、请求另一个微服务这些依赖一旦在 CI 环境里不稳定测试就跟着挂了。所以说Mock 的价值在于把测试边界划清楚你只测当前这段代码的逻辑是否正确至于它依赖的外部服务长什么样那是对方的事不在你这次测试范围内。我用个直白的例子你要测一个下单接口里“余额不足时返回错误码”的逻辑如果每次都真的去扣一次钱那测试就变成了破坏性测试哪怕你用的是沙箱环境也会有网络超时、并发冲突这些额外变量。把支付调用 mock 掉让它在代码里返回“余额不足”你才能稳定地验证自己的分支逻辑。这背后的设计原则是能用真实依赖就用真实依赖只在慢、贵、不稳定时才 mock。测试金字塔里单元测试层几乎可以全部 mock集成测试层尽量用真实的轻量替代比如 SQLite 代替 MySQL端到端测试才尽量走真实链路。现在很多团队做接口自动化一上来就把接口返回全部 mock 掉结果线上出了问题测试照样绿因为测试跑的是自己编的故事不是真实数据。这种“过度 mock”其实是一种失效的自动化你得当心。1.2 Mock、Fake、Stub 的概念区分Mock 这个词在使用中其实有点泛化严格说起来测试替身分好几种搞清楚它们的区别能帮你选对方案Stub桩只返回预设数据不记录调用过程。比如一个登录接口的桩你让它返回 token它就只有这一个功能。Fake假实现一个轻量级的真实实现。比如内存版数据库、假邮件发送器。能用 Fake 解决的问题不用 Mock。Mock模拟对象不只返回数据还能验证方法是否被调用、调用参数是什么、调用了几次。这才是 Mock 的核心能力。我见过不少测试代码把 Mock 当 Stub 用只配置返回值完全不做断言测试结果自然是“永远绿”。Mock 名字本身就包含“验证交互”的意思你用了 mock 却不去 assert_called_once_with等于白用。2. 核心细节解析与实操要点从 Mock 对象到 patch 的完整掌握2.1 Mock() 还是 MagicMock()这是个问题刚开始学 Mock最容易被问住的一个问题就是Mock 和 MagicMock 有什么区别简单说MagicMock 是 Mock 的子类它额外实现了对 Python 魔术方法的 mock 支持。什么叫魔术方法就是那些以双下划线开头和结尾的方法比如__eq__判断相等、__len__、__iter__、__getitem__这些。如果你的被测代码里会对 mock 对象做相等比较、取长度、遍历那用普通 Mock 会直接报错因为 Mock 没有实现这些特殊行为。我举一个实际场景被测函数对返回的数据做len(data) 0判断如果你用Mock()去模拟这个数据对象len()会抛TypeError: object of type Mock has no len()。但用MagicMock()它默认就会返回 0逻辑能顺利走下去。所以我的建议是拿不准就用 MagicMock除了极少数不需要魔术方法的场景MagicMock 基本是万能选择。注意MagicMock 对魔术方法也有自己的“默认行为”__len__返回 0__bool__返回 True__iter__返回空迭代器。这些默认值有时候会掩盖你代码里真实的 bug比如本来应该非空的数据因为 mock 默认长度是 0断言直接通过结果没测出数据为空时的错误分支。真项目里我给 mock 配返回值时都会写清楚return_value尽量不让默认值参与断言。2.2 return_value 和 side_effect一个管结果一个管行为配置 mock 的返回值最基础的做法是mock_obj MagicMock() mock_obj.get_user.return_value {id: 1, name: demo}但项目复杂之后你会发现 return_value 有点太“死”了。比如模拟一个接口“第一次调用抛异常第二次调用成功”return_value 就做不到。这时候要上 side_effect。side_effect有三种常见形态第一种抛异常。你直接传一个异常实例调用 mock 时就会抛出来mock_obj.call_api.side_effect TimeoutError(connection timeout)第二种传一个可迭代对象。每次调用依次取一个值适合模拟“第一次失败、第二次成功”这样的重试场景mock_obj.call_api.side_effect [TimeoutError(timeout), {code: 0, data: ok}]第三种传一个函数。函数接收调用参数并返回结果这是最灵活的方式适合根据入参动态生成返回def fake_response(url, **kwargs): if user in url: return {id: 1, name: demo} return {code: 404} mock_obj.request.side_effect fake_response我要提醒一个细节side_effect和return_value同时存在时side_effect优先。如果side_effect是函数或列表return_value根本不会起作用如果side_effect是异常那不管怎么调用异常都会立刻抛出。实际写测试的时候这两个别混着配容易让后来的人看不懂你的意图。2.3 patch 的三种用法装饰器、上下文管理器、手动启停patch是 pytest / unittest 里最核心的 mock 工具它本质上是把某个模块下的名字临时替换成 mock 对象测试结束后再恢复。最常见的是用装饰器from unittest.mock import patch patch(module_a.service.BillingAPI.charge) def test_order_insufficient_balance(mock_charge): mock_charge.return_value {code: 1001, msg: balance not enough} # 被测代码注意装饰器参数是“导入路径的字符串”不是已经 import 进来的对象。这个细节我单独展开讲mock 的是“目标模块里被使用时的名字”而不是定义处。比如from other_module import BillingAPI之后BillingAPI 这个名字已经绑定到了当前模块的命名空间你要 mock 的是当前模块.BillingAPI而不是other_module.BillingAPI。这是个经典坑我后面常见问题里会再说。有的场景装饰器不够用比如你只想在测试中段的时候临时替换后面又恢复。这时候用上下文管理器def test_ctx(): with patch(module_a.service.BillingAPI.charge) as mock_charge: mock_charge.return_value {code: 1001} # do something第三种是手动启停适合在 setup 阶段统一 mockteardown 阶段统一恢复def setup_method(self): self.mock_charge patch(module_a.service.BillingAPI.charge).start() self.mock_charge.return_value {code: 0} def teardown_method(self): patch.stopall()我个人在项目里用得最多的是上下文管理器因为它作用域清晰不会因为装饰器顺序影响可读性。用法上有个小技巧装饰器和上下文管理器混用时装饰器参数离函数最近的是第一个参数它有严格的从下到上、从右到左的顺序层数多的时候非常容易混淆。如果你发现自己已经叠了三层patch我建议直接拆成with patch嵌套或者用fixture来处理可读性会好很多。2.4 patch.object 和 patch.dict更精细地替换属性与字典patch默认是替换一个名字如果对象是个实例属性或者字典值就需要更精确的工具。patch.object用于替换某个对象的属性class Config: timeout 5 patch.object(Config, timeout, 10) def test_timeout_config(): assert Config.timeout 10我的经验是patch.object在测试依赖全局配置的代码时特别好用。比如服务端口、重试次数、开关状态都可以通过这种方式临时改掉测试结束自动恢复。patch.dict用于替换字典里的值最适合操作环境变量import os from unittest.mock import patch patch.dict(os.environ, {ENV: dev, API_KEY: test-key}) def test_env(): assert os.environ[ENV] dev在接口自动化里我用这个技巧做过不少事模拟生产环境配置、切换不同的数据库连接串、伪造密钥等。patch.dict还有个参数clearFalse如果设置成 True会先把原字典清空再注入新值通常不建议用容易误伤其他环境变量。2.5 spec 参数与 autospec别让 mock 悄悄接受不存在的属性Mock 有一个让新手困惑的特性它几乎接受任何属性和方法的访问不存在的方法也会返回一个子 mock。比如mock_obj Mock() mock_obj.nonexistent_method() # 不会报错返回 Mock 对象这在测试里是个隐患。假设被测代码误调用了一个不存在的方法如果是真实对象早就抛AttributeError了但 mock 会默默返回一个假对象测试继续往下走直到后续逻辑出现更离奇的错误排查的时候非常痛苦。解决办法是给 mock 加spec参数让 mock 只允许访问真实对象本来就有的属性class UserService: def get_user(self, uid): ... def delete_user(self, uid): ... mock_service Mock(specUserService) mock_service.get_user(1) # 正常 mock_service.update_user(1) # 抛 AttributeErrorpatch也支持autospecTruepatch(module_a.service.UserService, autospecTrue) def test_user(mock_service): mock_service.return_value.get_user.return_value {id: 1}这里autospecTrue会自动根据真实对象创建 spec避免你 mock 出一个“不存在的接口”。说句实在话spec这种能力在多人协作的项目里价值很大它能尽早暴露调用方代码里的拼写错误和接口漂移问题我强烈建议在测试公共模块时都加上。3. 实操过程与核心环节实现从简单 mock 到复杂场景的全流程推进3.1 用 fixture 做 mockpytest-mock 的 mocker 到底好在哪pytest 本身没有内置 mock 的 fixture要用unittest.mock直接写代码倒也过得去。但当你需要同一份 mock 配置在多个测试里复用时单纯靠 patch 装饰器就会让函数签名越来越臃肿。这个痛点正是pytest-mock插件要解决的。pytest-mock提供了一个名叫mocker的 fixture用法如下def test_order(mocker): mock_charge mocker.patch(module_a.service.BillingAPI.charge) mock_charge.return_value {code: 1001, msg: balance not enough} result order_api.create(amount100) assert result[code] 1001mocker.patch和直接unittest.mock.patch的区别主要是由 mocker fixture 管理的 patch测试结束时会自动全部恢复不需要手动写patch.stopall()。这一点在测试文件里一堆patch装饰器时特别省心不用担心某个 mock 泄漏到下一个测试里。更关键的是mocker 还能结合 fixture 做“注入式”的 mock。比如你有一个需要依赖第三方接口的服务类可以在 fixture 里把它的依赖 mock 好再返回给测试函数pytest.fixture def order_service(mocker): svc OrderService() mocker.patch.object(svc, _call_payment, return_value{code: 0, payment_id: p123}) return svc def test_order_created(order_service): result order_service.create(amount99) assert result[payment_id] p123这个模式在我看来是 pytest 生态里最容易“上瘾”的写法fixture 负责环境准备mock 负责依赖隔离测试函数只关心断言。代码结构清晰后期维护成本极低。3.2 异步接口的 MockAsyncMock 与各自的用法现在的服务端开发async/await 几乎是标配。用传统的 MagicMock 去 mock 异步函数会踩坑调用一个 async 函数返回的不是 coroutine 而是一个普通 MagicMock然后被测试的代码里await mock_result直接报TypeError: object MagicMock cant be used in await expression。解法是用unittest.mock.AsyncMock它能正确模拟异步函数的行为from unittest.mock import AsyncMock async def fetch_data(): pass # 真实实现 def test_async(mocker): mocker.patch(module_a.fetch_data, AsyncMock(return_value{code: 0, data: ok})) # 然后在异步测试里 await 调用跑异步测试要装pytest-asyncio测试函数加上pytest.mark.asyncio或用asyncio.run()包一层。我的经验是在一个中大型项目里异步 mock 的场景往往和async with上下文管理器绑在一起。比如要 mock 一个异步的数据库连接mock_conn AsyncMock() mock_conn.__aenter__.return_value mock_conn mock_conn.execute.return_value [{id: 1}] with patch(module_a.db.get_connection, return_valuemock_conn): result await some_service.query()这段代码的关键在于__aenter__也要返回一个 AsyncMock这样async with才能正常进入。如果你发现 mock 的 async 管理器进不去多半是忘了配__aenter__。实际操作中我用嵌套 AsyncMock 时会把这两行写在一起一眼就能看出意图。3.3 接口自动化里的 Mock 实战不依赖真实环境的联调利器很多做接口自动化的同学测试用例里最头疼的就是第三方接口实时汇率、天气、短信验证码、微信支付回调。这些接口要么本地连不上要么有调用次数限制要么数据不可控。Mock 在这块的使用方式不是简单地把返回写死而是可以组织成一套“可切换”的策略。我的工程实践是环境变量控制 mock 开关测试代码统一走一个 mock 工厂。以一个请求第三方天气接口的例子说明import os import requests from unittest.mock import patch def get_weather(city): resp requests.get(fhttps://api.weather.com/v1/{city}) return resp.json() def test_weather(mocker): mock_resp mocker.Mock() mock_resp.json.return_value {city: shenzhen, temperature: 28} mocker.patch(requests.get, return_valuemock_resp) assert get_weather(shenzhen)[temperature] 28这里mocker.patch(requests.get, return_valuemock_resp)是关键你在测试里把requests.get换成 mock被测代码里的requests.get用的就是同一个替换后的对象。这就是为什么“接口自动化里 mock 第三方接口”这么简单粗暴requests 是个全局对象patch 掉它的 get 方法所有调用就都走 mock 了。更贴近真实项目的场景是被测接口依赖另一个内部微服务但这个微服务在测试环境经常不稳定。这种情况下用 mock 模拟依赖服务的返回值可以让自动化用例不受上游影响。我自己维护过的一套接口自动化是这么组织的先按模块建 fixtures把常用的依赖 mock 抽象成 fixture 函数。在用例层通过 fixture 组合灵活替换不同接口的返回。在 allure 报告里给每个 mock 打上标签方便排查是走了 mock 还是真实调用。还有一个使用细节我必须提在接口自动化里mock 返回的 JSON 结构尽量和真实接口保持完全一致尤其是字段类型。我见过太多 mock 返回{code: 0}真实接口返回{code: 0}到了断言阶段才发现类型不匹配误报了十几条用例。写 mock 数据时最好先跑一次真实环境抓到真实返回样例再基于这个结构造数据。3.4 side_effect 的进阶用法从固定返回到动态行为前面提到了 side_effect 的三种形态这里单独拿一小节来说因为它几乎是 mock 进阶的分水岭。很多复杂的测试场景一句话就能用 side_effect 搞定。模拟“第一次调用抛超时第二次调用成功”的代码写成mock_request.side_effect [ TimeoutError(first timeout), {code: 0, data: retry success}, ]模拟“根据传入参数返回不同结果”可以写成函数def side_effect(url, **kwargs): if login in url: return {token: abc123} if profile in url: return {name: demo, role: admin} return {code: 404} mock_api.get.side_effect side_effect还有更复杂一点的场景某个函数内部连续调用同一 mock 多次你想对每次调用做不同的响应可以用带调用计数器的函数call_count 0 def dynamic_response(*args, **kwargs): nonlocal call_count call_count 1 if call_count 1: return {status: pending} return {status: done}我看过不少测试代码为了这种场景硬写了 if/else 去判断mock_obj.call_count其实完全没有必要side_effect 函数本身接收参数你在函数内部用参数判断逻辑才是更优雅的方案。3.5 复杂调用链的多层 mock一个用例涉及多个协作者真实项目里一个函数常常依赖四五个协作者一个缓存、一个数据库、一个消息队列、一个外部 API。写测试时如果全都靠patch装饰器一层层叠函数签名会变成patch(module_a.cache.Redis) patch(module_a.db.Session) patch(module_a.mq.Producer) patch(module_a.api.PaymentClient) def test_big_flow(mock_pay, mock_mq, mock_db, mock_cache):这种代码读起来不仅累而且装饰器顺序稍微搞错参数位置就全乱了。我的经验是不要贪图这种“一步到位”的写法改用 fixture 组合pytest.fixture def mock_redis(mocker): return mocker.patch(module_a.cache.Redis) pytest.fixture def mock_db(mocker): return mocker.patch(module_a.db.Session) pytest.fixture def mock_pay(mocker): mocker.patch(module_a.api.PaymentClient.charge, return_value{code: 0}) return mocker def test_big_flow(mock_redis, mock_db, mock_pay): result big_service.run() assert result[status] success这样一来测试函数签名清爽了每个 fixture 也可以单独复用到其他测试里。而且我建议在 fixture 内部直接配置好默认 return_value这样测试函数里只需要关注对返回值的断言不用再重复设置 mock 行为。只有少数特殊用例才需要覆盖 fixture 的默认值可以用mocker.patch在测试内部二次拦截。还有一种场景是“一个 mock 对象要被多个被测对象引用”比如redis_client同时用在 service 和 repository 两个模块里。这时候比较稳妥的做法是在 conftest.py 里定义 session 级别的 fixture确保整个测试会话只创建一个 mock 对象再把这个 mock 对象同时注入到所有被测模块里。这样才能保证 service 写入的值和 repository 读到的值用的是同一个 mock 对象避免“service 调 A mockrepository 读 B mock”这种各自为政的问题。4. 常见问题与排查技巧实录那些最容易让你怀疑人生的坑4.1 Mock 相关的 10 个高频问题速查表我在带团队和帮同事 review 测试代码的过程中总结了下面这些高频问题做成速查表方便你随时翻问题现象根本原因推荐解法mock 没生效真实代码还在跑patch 的路径写错了mock 的不是目标模块里的名字检查 patch 的字符串路径确认是“被测代码导入路径”调用 mock 方法时 TypeError: cant use in await expression用 Mock 去 mock async 函数改用 AsyncMock被测方法取不到 mock 返回值mock 对象的 return_value 没有配置或者 side_effect 覆盖了 return_value配置好 return_value检查 side_effect 优先级前一个测试注入的 mock 污染了后一个测试mock 没有自动 reset用 pytest-mock 的 mocker fixture 自动清理mock 对象可以访问不存在的属性导致误判Mock 默认接受任意属性加上 spec/autospec 参数断言assert_called_once失败但明明只调了一次被测代码可能调用时传了默认参数用assert_called_once_with打印实际调用参数环境变量改不掉用os.environ[KEY] ...后没有恢复用patch.dict管理环境变量mock 一个对象但被测代码调用的却是另一个对象两个模块各自 import 后绑定了不同名字统一通过 fixture 注入或用 patch.objectside_effect 是异常列表第一次调用没抛把异常对象和异常类搞混了传入异常实例如TimeoutError(timeout)多层 patch 装饰器参数顺序错乱装饰器顺序与函数参数顺序相反改用with patch嵌套或 fixture 组装4.2 排查“mock 没生效”的标准化流程我每次遇到 mock 不生效都会按下面这套流程排查基本能定位到 90% 的问题第一步确认 patch 的路径。路径不是“类定义的地方”而是“被测代码里调用时所在模块的命名空间”。举个例子被测文件src/order.py里写了from utils.wechat import WechatPay那 patch 就要写patch(src.order.WechatPay)而不是patch(utils.wechat.WechatPay)。第二步确认被测代码确实 import 了同一个对象。如果被测代码是import utils.wechat再调用utils.wechat.WechatPay()那你 patchutils.wechat.WechatPay是可行的但如果被测代码是from utils.wechat import WechatPay那它已经把引用绑定到了自己的命名空间必须 patchorder.WechatPay。第三步在 mock 上临时加一个副作用来验证是否生效。比如mock_obj.side_effect AssertionError(mock is working)如果测试抛出了这个异常说明 mock 生效了如果没抛说明被测路径根本没调用到这里。第四步检查是不是在 import 时对象就已被提前创建。有些代码在模块顶层就client WechatPay()后面所有方法都调这个模块级 client。这时候 mock 类本身没用得直接patch(order.client)或者用patch.object(order, client)。这套流程看着简单但真排查起来非常省时间。我见过太多人卡在第三步就放弃了直接在群里问“为什么 mock 没生效”其实只要加一行side_effect AssertionError就能验证。4.3 避免“测试全在测自己编的故事”这个坑不是技术问题而是方法论问题。mock 用得越多测试离真实行为越远最后可能出现“所有测试都绿但一上线就挂”的极端情况。我的建议是要明确 mock 的边界第一单元测试里 mock 外部依赖没问题但核心业务逻辑尽量不要 mock。比如金额计算、状态转换、权限判断这些关键规则应该用真实对象和真实数据去测mock 只负责隔离 IO。第二接口自动化里的 mock尽量只用在下游不稳定或不可控的场景。能走测试环境的真实依赖就不要 mock。比如测试环境本来就有 MySQL、Redis 这些基础设施用真实的服务去跑集成逻辑比 mock 出来更可靠。第三mock 的返回数据要定期和真实接口对齐。我在项目里会建一个“mock 数据基线”每次真实接口的返回结构有变更就同步更新 mock 数据并且用断言卡字段类型避免 mock 和真实接口脱节。说到底mock 是一把手术刀用得好可以精准切除测试里的外部依赖用不好就会把病灶整个挡住让测试变成一个“自嗨工具”。我始终相信一句话mock 是为了让测试更稳定而不是让测试失去灵魂。5. 工程落地与团队协作建议把 Mock 变成日常测试基础设施5.1 配置开关一条命令切换 mock 与真实环境在团队协作里最怕的不是某个人不会写 mock而是不同环境、不同人的本地配置各不相同导致同一套用例结果不一致。针对这个问题我推过一套基于环境变量的 mock 开关方案效果蛮明显。实现思路很简单在 conftest.py 里读取一个环境变量比如MOCK_ENABLED当它等于1时才自动加载 mock fixtures否则走真实依赖。import os import pytest pytest.fixture(autouseTrue) def auto_mock(mocker): if os.environ.get(MOCK_ENABLED, 0) 1: mocker.patch(module_a.api.PaymentClient.charge, return_value{code: 0}) mocker.patch(module_a.mq.Producer.send, return_valueTrue)这样本地开发时默认不开 mock遇到外部依赖问题可以手动开启CI 环境里按需开启。单个用例需要特殊 mock 时再显式覆盖。这种“默认真实按需 mock”的方式避免了一整套测试都活在 mock 世界里。5.2 conftest.py 的组织方式与 mock 命名规范conftest.py 是 pytest 的 fixture 集中地也是 mock 管理的最佳位置。我习惯把常用 mock 分成三类第一类基础环境类。比如环境变量、全局配置、日志对象。这一类几乎对所有测试都有影响用autouseTrue的 fixture 全局加载。第二类业务依赖类。比如外部 API 客户端、数据库 Session、Redis。这类在每个模块测试里按需引用。第三类定制行为类。比如某个用例要模拟接口超时、异常、慢响应。这类只写在具体测试文件里不放到全局。命名上我建议用mock_xxx开头并且在 fixture 内部把 return_value 都设置好到测试函数里只做断言。另外conftest.py 里不要塞太多具体用例逻辑否则文件会变成一个大杂烩后期没人敢动。5.3 和 pytest 生态工具整合allure 报告、超时控制、参数化Mock 在日常 pytest 工程里不是孤立的它经常和 allure 报告、pytest-timeout、参数化一起使用。这里分享两个我实际组合使用的场景。场景一allure 报告里记录“当前用例 mock 了哪些依赖”。做法很简单fixture 里给 allure 动态加标签import allure import pytest pytest.fixture def mock_payment(mocker): mocker.patch(module_a.api.PaymentClient.charge, return_value{code: 0}) allure.dynamic.tag(mock:payment) yield在报告里你能一眼看到这条用例依赖了哪些 mock排查“为什么这个环境没过”时非常有帮助。场景二参数化测试里复用 mock 配置。比如登录接口有十几种失败场景每种异常都要 mock 不同的返回pytest.mark.parametrize( error_code, expected_msg, [ (1001, user not found), (1002, password error), (1003, banned), ], ) def test_login_fail(mocker, error_code, expected_msg): mocker.patch(module_a.api.AuthService.login, return_value{code: error_code, msg: expected_msg}) resp login_api.login(demo, 123456) assert resp[msg] expected_msg这套组合拳在实际项目中非常常用参数化负责覆盖场景mock 负责隔离依赖allure 负责可观测性三个工具一配合测试用例的维护成本会降低许多。5.4 常见工程坑mock 数据版本管理最后说一个在“多人协作”场景下特别容易被忽视的问题mock 数据的版本管理。接口结构变更了mock 数据没更新测试一样是基于错误的契约在验证。如果你发现某个模块的测试一直绿但线上接口早就变了大概率就是 mock 数据没有同步。我目前的解决方法是把常用的 mock 返回数据抽成独立的 JSON 或 Python dict放在 tests/mock_data/ 目录下。在测试里用固定的 fixture 加载这些数据而不是散落在各个测试函数里。每次真实接口变更走正常的 review 流程更新 mock_data 文件和对应的契约测试。这么做的好处是mock 数据基线可以被检查、被审计不会出现一个人改了一个接口结构其他同事的测试全挂在老 mock 数据上的情况。我个人在实际操作中的一个体会是Mock 看起来是一个“小技巧”但真正把它用对、用好需要从方法论和工程层面一起发力。如果只是把 Mock 当作“造假数据”你会很快遇到边界问题如果你能把 mock 边界划清楚、把 fixture 组织好、把数据版本管理起来pytest 的测试体系会变得非常稳定且好维护。最后再分享一个小技巧每次写 mock 之前先想一想“如果这里不 mock真实环境会发生什么”想清楚这个问题你就知道该 mock 什么、不该 mock 什么了。
返回列表