首页 / 资讯中心 / 文章详情

Python Mock测试实战:隔离外部依赖的单元测试技巧

Python Mock测试实战:隔离外部依赖的单元测试技巧 ★ FEATURED ARTICLE
1. 为什么我们需要Mock测试先聊点实际的。做后端接口开发或者测试的同学应该都遇到过这么几个让人抓狂的场景前端页面还差个接口联调后端同事说“表结构还没定下周再联”第三方支付回调迟迟等不到测试环境的数据数据库里那条异常数据开发环境根本造不出来。以前遇到这种情况要么干等要么在代码里临时写死一堆乱七八糟的逻辑联调完再删不但麻烦还容易出事故。Mock测试解决的就是这类问题。简单说Mock就是在测试过程中用一个虚拟的、可控的对象去替换掉那些依赖的、复杂的、不容易构造的模块比如外部HTTP接口、数据库查询结果、消息队列的读写。它就像剧组里的“替身演员”在正式演员不方便出场的时候先保证拍戏流程能完整走一遍。我见过很多刚接触Mock的人第一反应是“这东西是不是用来造假数据的”只说对了一半。Mock的核心价值不是造假而是隔离依赖。你要测的是“当前这段代码”的逻辑是否正确而不是“下游服务”是否正确。把下游服务Mock掉之后你的测试就只关注自己的业务逻辑而且测试速度会快非常多。真实环境一个HTTP请求动不动几百毫秒Mock之后纯内存操作毫秒级就能跑完跑一千个用例也就是十几秒的事。用Python做Mock测试主流方案有三个层次最基础的是自己写假对象灵活但麻烦进阶是使用标准库unittest.mock日常够用内置Mock、MagicMock、patch这些核心工具再进阶就是pytest生态下的pytest-mock插件把mock和pytest的断言、fixture结合得非常好。这篇文章会把这三层都讲到但重心放在常用的unittest.mock和pytest-mock上因为这两个覆盖了绝大多数工作场景。适合看这篇文章的读者包括刚入职的测试工程师、写接口的Python后端开发、以及正在做单元测试但被外部依赖折磨的任何人。哪怕你之前完全没接触过Mock跟着代码一步步写也能在半小时内上手。文中的所有示例我都放在模拟项目场景里方便你直接改改用。2. 先搞懂Mock的核心设计思路2.1 Mock的本质替身与录制深入理解Mock前先把本质弄清楚。Mock的本质是两个动作替身和录制。替身很好理解就是用一个虚拟对象替换掉真实对象。但替换不是目的替换之后真正起作用的是录制。也就是说Mock对象会记录下你调用了它哪些方法、传了什么参数、调了多少次然后根据你的预设去返回指定结果。举个生活化的例子。假设你开了一家奶茶店新来的店员还没上岗但你得先培训收银流程。这时候你会不会找个纸片人摆在收银台后面告诉顾客“先扫码、再点单、最后拿小票”纸片人就是Mock它不会做奶茶但它把整个收银流程走通了还记录下今天有多少顾客来点过单。对应到代码里被测试的代码调用了Mock对象的某个方法Mock会像收银员一样登记这次调用你调了谁、用的什么参数。之后你可以对这些登记信息做断言比如“这个方法必须被调用一次”“必须用某个特定参数调用”这就是Mock测试的核心玩法。明白了替身和录制这两个词再看Mock配套的各种工具会顺畅很多。2.2 为什么Mock能提升测试质量很多人觉得Mock就是“测个寂寞”因为依赖全被替换了测的还是不是真实逻辑这个质疑有一定道理但前提是你把Mock用错了地方。Mock提升测试质量体现在三个层面第一确定性。真实的外部依赖是不确定的数据库可能在某个时刻宕机第三方接口可能突然超时网络可能抖动。这些不确定因素会直接导致测试结果不稳定——同一个用例上次过这次挂项目经理问起来你还没法解释。Mock掉这些外部依赖后测试结果完全确定你断言什么就是什么排除所有环境干扰。第二专注性。单元测试的基本要求是“只测一个单元”。如果你在测用户服务时却连带着把数据库连接、Redis缓存、消息通知全跑了一遍这已经不叫单元测试了叫集成测试。一旦出问题排查范围大得吓人。用Mock把周边依赖隔离掉你的测试范围严格锁死在当前代码单元上定位问题精准得多。第三可重复性。真实环境里测试数据是会被污染的。比如你执行了一次删除订单的用例订单真实删掉了下一次跑同样用例就找不到这个订单了。用Mock模拟删除结果不管跑多少次测试环境都是干净的完全不依赖外部数据状态。这在CI流水线上尤其重要——每次提交代码自动跑测试必须保证每个用例都可以反复执行。2.3 什么情况下应该用Mock也不是所有场景都适合Mock用错了反而帮倒忙。我用实践经验帮你总结一个决策表场景是否建议Mock原因单元测试只测单个函数/类的业务逻辑强烈建议隔离依赖保证确定性接口联调前后端协同开发建议后端未就绪时用Mock模拟返回第三方服务支付、短信、地图强烈建议收费/限量/环境难以自建数据库操作逻辑建议优先Mock真正的SQL覆盖交给集成测试框架本身的行为如路由分发不建议框架是自己的职责Mock了等于没测自己刚写的代码逻辑不建议先测自己再隔离别人这里有个关键心态要摆正Mock是测试的辅助手段不是偷懒工具。建议Mock的时候大胆Mock但核心业务逻辑该真实跑的还是得真实跑否则你的测试就是空中楼阁上生产必出事。3. Python Mock测试必会的基础操作3.1 环境准备不需要安装额外的重型框架Python 3.6以上都内置了unittest.mock。如果你用pytest写用例建议再装一个pytest-mock平时开发装依赖时顺手带上就行pip install pytest pytest-mock如果你用的是unittest那就零依赖直接开写。我自己更推荐用pytest配合pytest-mock写测试原因有三个断言失败的信息更直观fixture体系让测试数据复用非常方便而且pytest-mock对Mock对象的断言也做了封装写起来更简洁。当然如果你所在项目已经在用unittest那也完全没问题核心Mock逻辑是一模一样的。3.2 从Mock类开始造一个虚拟员工Mock类是Python mock体系的基石。所有神奇的替身能力都从这个类开始。先看一段最简单但最核心的示例from unittest.mock import Mock # 创建一个Mock对象当作一个虚拟员工 mock_employee Mock() # 预设这个员工的“工作方法”比如上班打卡 mock_employee.check_in.return_value 打卡成功 mock_employee.work.return_value 完成了今日任务 # 模拟业务调用 result1 mock_employee.check_in(09:00) result2 mock_employee.work(日报) print(result1) # 打卡成功 print(result2) # 完成了今日任务这段代码展示了Mock最基本的用法给任意方法预设返回值然后像正常函数一样调用它。你自己不用写任何类实现Mock对象就像万能积木你要什么方法它就能有什么方法。但是Mock真正强大之处在于调用跟踪。继续用上面的例子# 检查员工是否被要求加班过 mock_employee.work(加班) mock_employee.work(周末值班) # 查看调用信息 print(mock_employee.work.call_count) # 3日报、加班、周末值班 print(mock_employee.work.call_args) # 最后一次调用的参数 print(mock_employee.work.call_args_list) # 所有调用的参数列表call_count记录总调用次数call_args记录最后一次传参call_args_list记录全部传参。这三个属性在做断言时极为有用等于你有了一个全程监控摄像头被测试代码有没有调用目标方法、传的参数对不对一眼就能确认。3.3 MagicMock是什么和Mock有什么区别新手最容易困惑的就是Mock和MagicMock的区别。简单说MagicMock是Mock的超集它额外预置了很多Python魔法方法的默认行为。魔法方法就是那些双下划线包起来的方法比如__iter__、__getitem__、__str__、__enter__。普通Mock对象遇到这些魔法方法会比较呆板如果你要Mock一个文件对象或者一个字典对象往往需要手动配置好几个魔法方法。而MagicMock把这些魔法方法的默认返回值都设置好了。比如它的__iter__默认返回空迭代器__getitem__默认返回新的MagicMock__len__默认返回0用起来基本不用做额外配置。我的建议是绝大多数场景直接用MagicMock。虽然它“太智能”可能导致你漏掉一些边界验证但省下的配置时间足够抵消这点小瑕疵。只有在极少数需要严格验证魔法方法调用的场景才需要回归普通Mock。日常写代码MagicMock就是那个最好用的“万能替身”。3.4 patch动态替换指定位置的Mock光会创建Mock对象还不够最关键的是如何把Mock“安插”到被测试的代码里去。这就轮到patch出场了。patch有三种主流用法装饰器、上下文管理器、手动start/stop。我来逐一演示。先看场景我们要测试一个订单服务它内部调用了支付模块的接口。支付接口当然是外部服务需要Mock掉。# payment.py 模拟外部支付模块 def pay(order_id, amount): # 此处实际调用第三方支付网关 return {status: success, transaction_id: real_txn_001} # order_service.py 订单服务依赖支付模块 from payment import pay def process_order(order_id, amount): print(f开始处理订单{order_id}) result pay(order_id, amount) if result[status] success: return f订单{order_id}支付成功流水号{result[transaction_id]} else: return f订单{order_id}支付失败用装饰器方式Mock掉pay方法from unittest.mock import patch patch(payment.pay) def test_process_order(mock_pay): # 预设Mock的返回值 mock_pay.return_value {status: success, transaction_id: mock_txn_123} # 调用被测函数 result process_order(A001, 99.9) # 断言 assert result 订单A001支付成功流水号mock_txn_123 mock_pay.assert_called_once_with(A001, 99.9)这里要注意一个极其关键的细节patch的字符串参数写的是payment.pay不是order_service.pay。这是因为process_order函数内部通过from payment import pay的方式把pay这个名字绑定到了order_service模块的命名空间里。patch替换的必须是“被测试代码实际去查找名字的命名空间”。如果你在这段代码里写patch(pay)或者patch(payment.pay)后却不起作用八成就是命名空间搞错了。这是Mock测试最经典的坑我放到后面常见问题里再细讲。用上下文管理器写同样的逻辑from unittest.mock import patch def test_process_order_context(): with patch(payment.pay) as mock_pay: mock_pay.return_value {status: success, transaction_id: mock_txn_456} result process_order(A002, 199.0) assert result 订单A002支付成功流水号mock_txn_456装饰器和上下文管理器的区别在于作用范围控制。装饰器作用于整个测试函数上下文管理器可以灵活控制Mock的生命周期。如果你只想在代码中间某一段生效后面恢复正常逻辑就用上下文管理器。手动start/stop的方式适合在setUp和tearDown里用或者需要同时patch很多个对象的时候from unittest.mock import patch class TestOrderService: def setUp(self): self.patcher patch(payment.pay) self.mock_pay self.patcher.start() self.mock_pay.return_value {status: success, transaction_id: mock_txn_789} def tearDown(self): self.patcher.stop() def test_process_order(self): result process_order(A003, 299.0) assert result 订单A003支付成功流水号mock_txn_789三种方式任选其一即可没有绝对的好坏看场景哪样写着顺手。我个人的习惯是单个mock用上下文管理器同时mock多个或者一个测试类里所有用例都要用时就写在setUp里。3.5 让Mock学会抛异常side_effect预设返回值只能模拟“正常情况”但测试里最重要的往往不是正常路径而是异常路径。第三方接口超时、数据库断连、网络不可达——这些通通是线上最常见的故障。side_effect就是用来模拟异常和复杂行为的利器。from unittest.mock import Mock, patch def test_payment_timeout(): with patch(payment.pay) as mock_pay: # 让mock在调用时抛超时异常 mock_pay.side_effect TimeoutError(支付网关超时) try: process_order(A004, 99.9) except TimeoutError: print(捕获到了超时异常符合预期)side_effect也可以是列表按调用顺序依次返回不同的结果。这个特性在模拟“第一次失败、第二次成功”的重试场景时非常好用from unittest.mock import Mock mock_retry Mock() mock_retry.side_effect [ConnectionError(第一次连接失败), 第二次连接成功] # 第一次调用抛异常 try: mock_retry() except ConnectionError: print(第一次调用失败了) # 第二次调用返回成功 print(mock_retry()) # 第二次连接成功甚至可以直接传一个函数根据输入参数动态决定返回值def mock_price(item_name): if item_name 苹果: return 5.5 elif item_name 香蕉: return 3.2 return 0 mock_query Mock(side_effectmock_price) print(mock_query(苹果)) # 5.5 print(mock_query(香蕉)) # 3.2这里side_effect和return_value同时存在时side_effect优先级更高。也就是说只要设置了side_effectreturn_value就只会兜底日常写的时候要注意别混用导致结果和预期对不上。4. 手把手示例搭建一个带Mock的完整测试4.1 模拟项目场景前面讲了一堆基础概念下面咱们动真格搭一个完整的demo。假设我们在开发一个“用户下单并发送通知”的服务。这段服务代码依赖三个外部组件用户信息查询数据库、库存扣减Redis、短信通知第三方短信平台。三个外部依赖都要Mock掉只测服务自身的流程逻辑。先准备被测试模块order_service.py# order_service.py import requests class UserService: def get_user_info(self, user_id): # 真实场景查询数据库 url fhttp://user-service/api/users/{user_id} resp requests.get(url) return resp.json() class StockService: def deduct_stock(self, product_id, quantity): # 真实场景操作Redis扣减库存 return {success: True, remaining: 100 - quantity} class SmsService: def send_sms(self, phone, content): # 真实场景调用第三方短信平台 resp requests.post( http://sms-service/send, json{phone: phone, content: content} ) return resp.status_code 200 class OrderFacade: def __init__(self): self.user_service UserService() self.stock_service StockService() self.sms_service SmsService() def create_order(self, user_id, product_id, quantity, phone): user self.user_service.get_user_info(user_id) if not user: return {code: 404, msg: 用户不存在} stock_result self.stock_service.deduct_stock(product_id, quantity) if not stock_result[success]: return {code: 500, msg: 库存不足} sms_result self.sms_service.send_sms(phone, f您的商品已下单成功) if not sms_result: return {code: 500, msg: 短信发送失败} return {code: 200, msg: 下单成功, data: {order_id: X001}}这个OrderFacade很典型它本身没有复杂计算就是编排了三个外部服务。单元测试的重点就是验证这个编排逻辑——顺序对不对、分支处理对不对、返回结果对不对。4.2 第1版测试分别Mock三个外部服务用unittest写第一个版本目标是把三个外部依赖全部Mock掉# test_order_service.py import unittest from unittest.mock import patch, MagicMock import order_service class TestOrderFacade(unittest.TestCase): def setUp(self): self.facade order_service.OrderFacade() patch.object(order_service.UserService, get_user_info) patch.object(order_service.StockService, deduct_stock) patch.object(order_service.SmsService, send_sms) def test_create_order_success( self, mock_sms, mock_stock, mock_user ): # 配置Mock返回值 mock_user.return_value {user_id: 1, name: 张三} mock_stock.return_value {success: True, remaining: 50} mock_sms.return_value True # 调用被测方法 result self.facade.create_order( user_id1, product_id101, quantity2, phone13800138000 ) # 断言结果 self.assertEqual(result[code], 200) self.assertEqual(result[data][order_id], X001) # 断言外部服务调用次数和参数 mock_user.assert_called_once_with(1) mock_stock.assert_called_once_with(101, 2) mock_sms.assert_called_once_with(13800138000, 您的商品已下单成功) patch.object(order_service.UserService, get_user_info) patch.object(order_service.StockService, deduct_stock) patch.object(order_service.SmsService, send_sms) def test_create_order_user_not_found( self, mock_sms, mock_stock, mock_user ): mock_user.return_value None result self.facade.create_order( user_id999, product_id101, quantity2, phone13800138000 ) self.assertEqual(result[code], 404) # 用户不存在时不应继续走库存和短信 mock_stock.assert_not_called() mock_sms.assert_not_called() patch.object(order_service.UserService, get_user_info) patch.object(order_service.StockService, deduct_stock) patch.object(order_service.SmsService, send_sms) def test_create_order_stock_fail( self, mock_sms, mock_stock, mock_user ): mock_user.return_value {user_id: 1, name: 张三} mock_stock.return_value {success: False, remaining: 0} result self.facade.create_order( user_id1, product_id101, quantity2, phone13800138000 ) self.assertEqual(result[code], 500) self.assertEqual(result[msg], 库存不足) mock_sms.assert_not_called()这里我用了patch.object来替换类里的方法。它和patch的区别在于patch.object(A, method)直接指定要替换的对象及其属性而patch(模块路径)则按字符串路径去找。对于替换类内部的方法patch.object写起来更直观也少了很多命名空间的歧义。另外注意装饰器的顺序patch.object离被测函数最近的参数在最前面。三个装饰器从下往上看最下面的是UserService它的mock参数也排在函数参数的第一位。这个顺序非常容易搞反建议每次写的时候心里默念“装饰器从下往上读参数从左到右排”。4.3 第2版测试用pytest-mock写出更简洁的用例如果你更习惯pytest那pytest-mock能把上面的代码再简化一个档次。先安装pip install pytest-mock然后测试代码变成这样# test_order_service_pytest.py import order_service from unittest.mock import patch def create_facade(): return order_service.OrderFacade() def test_create_order_success(mocker): facade create_facade() mock_user mocker.patch.object( order_service.UserService, get_user_info, return_value{user_id: 1, name: 张三} ) mock_stock mocker.patch.object( order_service.StockService, deduct_stock, return_value{success: True, remaining: 50} ) mock_sms mocker.patch.object( order_service.SmsService, send_sms, return_valueTrue ) result facade.create_order( user_id1, product_id101, quantity2, phone13800138000 ) assert result[code] 200 assert result[data][order_id] X001 mock_user.assert_called_once_with(1) mock_stock.assert_called_once_with(101, 2) mock_sms.assert_called_once_with( 13800138000, 您的商品已下单成功 )mocker.patch.object把“创建Mock对象”和“替换到指定位置”合成了一个方法而且函数结束时自动恢复原对象不需要手动stop。相比unittest方式少了setUp、tearDown这些样板代码每个用例自包含心智负担小很多。再写一个更复杂的用例验证“库存失败时不再调短信”def test_create_order_stock_fail(mocker): facade create_facade() mocker.patch.object( order_service.UserService, get_user_info, return_value{user_id: 1, name: 张三} ) mocker.patch.object( order_service.StockService, deduct_stock, return_value{success: False, remaining: 0} ) mock_sms mocker.patch.object( order_service.SmsService, send_sms, return_valueTrue ) result facade.create_order( user_id1, product_id101, quantity2, phone13800138000 ) assert result[code] 500 assert result[msg] 库存不足 mock_sms.assert_not_called()如果你是刚接触测试的小白我建议直接学pytest这套。社区现在是pytest占主导团队协作、CI集成、参数化这些能力都比unittest强不少Mock测试只是它顺手提供的一个小能力。4.4 跑测试和看覆盖率代码写完了怎么跑pytest最爽的一点就是命令行简单pytest test_order_service_pytest.py -v终端就会看到类似这样的输出collected 2 items test_order_service_pytest.py::test_create_order_success PASSED test_order_service_pytest.py::test_create_order_stock_fail PASSED再看看覆盖率安装pytest-covpip install pytest-cov pytest test_order_service_pytest.py --covorder_service --cov-reportterm-missing如果Mock写得好覆盖率数字会非常好看因为每个分支都被测到了。我强烈建议团队把覆盖率纳入CI门槛比如新代码覆盖率必须不低于80%。Mock测试是达成这个指标的关键工具不Mock外部依赖分支根本走不全。5. 那些坑了我很久的Mock细节5.1 patch路径写错Mock了等于没Mock这是Mock测试新手最容易踩的坑也是影响面最大的坑。前面说过patch替换的是“被测试代码实际去查找名字的命名空间”。很多教程都会提醒这一句但真正在项目里写的时候还是会搞错因为不同导入方式的行为完全不一样。# 方式一直接导入模块 import requests def fetch_data(): return requests.get(http://example.com/api)这种场景要patch的方法是requests.get因为函数体里用的是requests.get这个完整路径patch(requests.get)# 方式二from导入具体函数 from requests import get def fetch_data(): return get(http://example.com/api)这种场景如果你还patchrequests.getMock根本不会生效。原因在于fetch_data函数查找的是当前模块里的get名字它已经绑定到requests模块里的get函数对象上了。你要patch的应该是当前模块的命名空间# 假设当前模块叫 my_service.py patch(my_service.get)这就是所谓的“patch where its used, not where its defined”——在它被使用的地方去替换而不是在它定义的地方去替换。每次遇到Mock不生效首先检查这个点。项目里的模块路径比较深时推荐用patch.object它直接锁定类和属性没有字符串路径的歧义。比如前面用的patch.object(order_service.UserService, get_user_info)就不会出现找不到路径的问题。5.2 装饰器的顺序和参数顺序搞混上文的例子也提到过多个patch装饰器叠在一起时参数顺序非常反直觉。patch.object(UserService, get_user_info) patch.object(StockService, deduct_stock) patch.object(SmsService, send_sms) def test_create_order_success(self, mock_sms, mock_stock, mock_user):注意离函数最近的装饰器最下面的生成的mock参数排在最左边。这里的规律是装饰器从下往上依次注入所以最下面的装饰器对应第一个参数。如果写反了比如把mock_user和mock_sms的位置换一下你就会发现所有mock的返回值都不对了而且报错信息还不明显因为mock对象之间是可以互相指责的。我遇到过好几次这样的问题排查了半小时最后发现就是参数顺序反了。建议养成的习惯装饰器从上往下按调用顺序写参数从左往右就反过来了写完之后立刻核对一遍。除非你非常熟练先按“离函数最近的装饰器对应第一个参数”这个口诀写然后再去调整装饰器的上下位置。5.3 MagicMock太智能连不存在的属性都能返回MagicMock最“坑”的地方其实也正是它最强大的地方你访问任何不存在的属性它不会报错而是返回一个新的Mock对象调用任何不存在的方法它也不会报错同样返回一个Mock对象。这在简化配置的同时会掩盖一些问题。比如你不小心把方法名拼错了facade.create_order(user_id1, product_id101, quantity2, phone13800138000)假如你拼成了create_orden如果这个方法是Mock出来的它不会报AttributeError而是正常返回一个Mock对象。然后你的测试断言全部通过但一上生产就炸。所以使用MagicMock时一定要严格断言关键方法确实被调用了。如果发现测试用例里没有任何assert_called要警惕是不是Mock对象“悄悄吞掉了”错误。5.4 直接Mock requests库容易造成大量重复项目里到处直接用requests.get、requests.post的场景特别多每个测试都要写一遍patch(requests.get)重复又繁琐。这种情况下更好的做法是封装一个基础测试工具。以项目实际情况为例可以搞一个公共模块# test_utils.py from unittest.mock import patch, Mock import requests def mock_requests_get(json_data, status_code200): Mock requests.get返回指定JSON数据 mock_resp Mock() mock_resp.status_code status_code mock_resp.json.return_value json_data patcher patch.object(requests, get, return_valuemock_resp) patcher.start() return patcher这样在每个测试用例里就可以一行搞定def test_xxx(self): patcher mock_requests_get({code: 200, data: []}) # 执行测试... patcher.stop()封装的好处是团队里所有成员都统一用同一套Mock逻辑出了bug也只改一处。这篇博文虽然只写了个简单的mock_requests_get但在真实项目里基于这个思路可以把Redis客户端、消息队列、短信平台等各基础设施的Mock工具统一建起来。如果你用pytest还有更优雅的fixture方式# conftest.py import pytest from unittest.mock import patch, Mock pytest.fixture def mock_redis(): with patch(redis.Redis.get) as mock_get: mock_get.return_value cached_value yield mock_get测试函数里直接声明参数就能拿到def test_redis_logic(mock_redis): # 测试逻辑里调用redis.get会自动返回cached_value data get_data_from_redis(key) assert data cached_valuefixture还能通过参数控制返回值推荐的做法是在fixture里加一个请求参数pytest.fixture def mock_redis(mocker, request): with patch(redis.Redis.get) as mock_get: mock_get.return_value getattr(request, param, cached_value) yield mock_get然后测试函数上用pytest.mark.parametrize传不同参数就可以一个用例测多种情况非常方便。5.5 嵌套Mock时别忘了层级关系有些对象不是一层就结束的。比如requests.get().json()如果你粗暴地只Mock了requests.get返回一个普通对象那.json()就会报AttributeError。正确做法是用嵌套Mockmock_resp Mock() mock_resp.json.return_value {code: 200, data: hello} with patch(requests.get, return_valuemock_resp): # 被测试代码中调用requests.get(...).json() 会返回 {code: 200, data: hello}这个技巧扩展到很多场景数据库连接的cursor().execute()、Redis的client.redis().get()都需要理清楚层级关系逐层Mock。至少要在心里画一张调用链图确认哪些方法会返回对象、哪些方法在返回对象上继续被调用。6. 进阶技巧用Mock模拟时间和随机数6.1 控制时间来保证测试稳定业务代码里判断时间很常见促销过期、超时重试、限流窗口。直接靠真实时间测试用例结果会随当前时间变化CI半夜跑测试就翻车。用Mock把时间控制住from unittest.mock import patch import time def is_promotion_valid(end_timestamp): now time.time() return now end_timestamp def test_promotion_valid(): with patch(time.time, return_value1000.0): # 固定当前时间是1000秒 assert is_promotion_valid(2000.0) is True assert is_promotion_valid(500.0) is False把time.time固定住之后同一段代码在任何时刻跑结果都一样。这对业务中带时间判断的测试是救命的。6.2 控制随机数让测试可复现抽奖、随机分配、验证码之类的业务随机数直接影响分支走向。测这类代码就必须让随机数“固定”下来。from unittest.mock import patch import random def lottery(): return 一等奖 if random.random() 0.1 else 谢谢参与 def test_lottery_win(): with patch(random.random, return_value0.05): assert lottery() 一等奖 def test_lottery_lose(): with patch(random.random, return_value0.95): assert lottery() 谢谢参与这种做法保证了测试的确定性。生产环境的随机逻辑照旧只有测试环境被Mock掉业务不受任何影响。6.3 用Mock测试幂等性很多系统要求接口幂等——同一请求重复多次结果一致、数据不重复。用Mock模拟同一个参数多次调用是验证幂等性最快的方法def test_idempotent(mocker): mock_check mocker.patch.object(OrderService, check_duplicate) mock_check.return_value True # 第一次调用认为订单已存在 # 业务逻辑应该跳过重复插入 # 再调用一次结果应该完全一致 result1 process_order(O001) result2 process_order(O001) assert result1 result2幂等性测试对支付、下单这类关键链路非常重要线上出问题的概率也最高。Mock让这个测试变得异常简单——不需要真的请求两次外部服务只需要模拟两次同样的调用场景。7. 常见问题排查与团队落地经验7.1 Mock没生效的排查清单我整理了一份排查顺序按这个顺序查基本能找到90%的问题排查点检查方法patch目标路径确认patch的是“被使用处”的命名空间不是“定义处”插件/装饰器顺序确认装饰器从下往上对应参数顺序patch作用域结束确认Mock只在patch作用域内生效出了作用域就恢复类 vs 实例确认patch.object针对的是类还是实例两者作用范围不同拼写错误MagicMock不会报错主动加assert_called验证多进程/多线程确认线程/进程内也执行了相同patch7.2 mock在外包接口联调中的应用前面聊的都是单元测试。Mock另一个人尽皆知但常常没被好好利用的场景是前后端联调和多团队协作。后端接口还没写好前端已经等着联调了怎么办最典型的方式是用Mock搭一个临时服务按照接口文档返回假数据。Python里的unittest.mock.Mock不适合做HTTP mock server但配合flask可以快速搭# mock_server.py from flask import Flask, jsonify, request app Flask(__name__) app.route(/api/user/int:user_id, methods[GET]) def mock_user(user_id): # 完全按接口文档定义返回 return jsonify({ user_id: user_id, name: Mock用户, level: 3 }) app.route(/api/order, methods[POST]) def mock_order(): data request.get_json() return jsonify({ code: 0, order_id: MOCK_ str(data.get(user_id, )), msg: success }) if __name__ __main__: app.run(port5001)前端联调时把接口地址指向http://localhost:5001后端的开发并行推进两边谁都不等谁。这层Mock服务一般放项目根目录的mock/文件夹里由前端或测试同学维护接口文档一更新mock同步更新。还有更专业的工具比如mockoon、json-server甚至可以直接在postman里建mock server。但说实话用Python自己写个小服务反而是最灵活的逻辑复杂一点也能应付进代码仓库后还方便别人评审。7.3 测试金字塔与Mock的边界接触Mock久了自然就会思考一个问题什么时候该Mock什么时候不该Mock这里我建议每个团队都校准一下测试金字塔的意识。金字塔的层次是底层单元测试数量最多中层服务/组件测试顶层端到端测试最少。Mock应该主要用在底层单元测试——被测目标单一时隔离它的外部依赖。而端到端测试层尽量少用Mock这个层次的价值就在于真实链路包括数据库、第三方服务都该是真的。如果把端到端测试也Mock掉测试的意义就打折了。这就引出一个实操原则一个模块如果有一层“对外协议”比如网络请求、数据库读写那一层可以大胆Mock但如果业务逻辑本身就依赖这些组件的内部实现细节那Mock带来的维护成本会非常高这时候你真正需要的是重构代码把依赖接口化而不是硬着头皮Mock。我做过的几个项目里都有类似的经历测试难写不是因为Mock技巧不够而是生产代码的耦合太高。等你意识到这一点去调整生产代码的结构将依赖注入到类里反而测试好写了很多。所以写Mock有时候是一种“代码异味探测器”——如果你发现某个模块非常难Mock大概率它是真的需要重构。8. 收尾Mock测试的长期价值最后再分享一点个人的心得体会。刚开始用Mock那段时间我曾经陷入一个误区想用Mock“包办一切”。为了跑通测试到处打补丁连自己写的核心业务方法都Mock。结果测试是绿了但项目一上线就暴露出许多逻辑错误自动化测试形同虚设。踩过几次坑之后我才慢慢想明白一个道理Mock是帮你“排除干扰”的不是帮你“跳过思考”的。Mock掉的应该是你不关心的噪音而不是你要负责的链路。凡是属于你自己业务逻辑的分支、判断、计算该测的必须真实测Mock永远只是辅助。另外一件很重要的事就是Mock测试的维护成本比普通单元测试高。因为生产代码改了方法签名mock的断言也会跟着变。所以团队引入Mock测试时最好把Mock测试用例当成代码一样做code review社区讨论时也建议约定一个规则mock目标和方法签名要写在注释里方便其他人理解。还有个小技巧Mock测试的命名可以带上业务场景比如test_create_order_when_user_not_found_should_return_404比test_create_order_fail这种看得明白得多。以后回归测试挂了扫一眼测试名字就能定位到是哪条业务链路出了问题。这篇文章从Mock的本质讲到实战再讲到进阶技巧内容比较多如果你刚入门建议先照着第4节的示例把整个流程跑通再去琢磨那些高阶玩法。Mock本身不是目的让团队能够快速、确定、低成本地验证代码逻辑才是真正有价值的东西。这套工具在自己的代码库里待久了你会越来越依赖这种“可控的测试方式”它带给你的信心积累是真实环境验证完全比不上的。
阅读完成 · 觉得有帮助?
咨询建站