ARTICLE DETAIL

资讯详情

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

Mock与Spy深度解析:自动化测试依赖隔离的正确姿势

Mock与Spy深度解析:自动化测试依赖隔离的正确姿势 在自动化测试里摸爬滚打久了你会发现一个很有意思的现象测试挂了第一反应不是去翻业务代码而是去查mock有没有配错测试全绿了心里却有点发慌因为代码逻辑明显改坏了测试居然还通过。这种“绿得可疑”的时刻多半就是Mock和Spy使用姿势出了问题。很多人把Mock和Spy当成差不多的东西觉得都是“替身演员”但这两者的本质差异直接决定你到底是在真实隔离依赖还是在用一个精致的假象糊弄自己。今天这篇就来扒一扒Mock和Spy的真面目结合pytest、Mockito这些常用到的工具把测试隔离依赖这件事讲透。无论你是刚入门自动化测试的新手还是已经写了大量接口测试、单元测试的老手读完之后应该能回答几个问题什么时候该用Mock什么时候该用Spy以及最要命的一件事——你的测试到底在验证什么。1. 先搞清楚Mock和Spy到底是什么1.1 Mock替身演员还是玩具假人Mock的核心思路是用模拟对象替代真实依赖设置好期望行为然后验证调用关系。Mock本身不执行真实逻辑它就是一个占位对象完全由你在测试里定义“该出现什么结果”。举个例子。你测试一个下单服务需要调用支付网关。测试场景里支付网关可能返回成功、失败、超时三种结果。如果走真实网关你根本没法在本地稳定重现这三种情况。Mock的价值就在这里你把支付网关这个外部依赖“锁死”成你想要的状态才能真正测到自己的业务逻辑分支。但这也埋了一个隐患如果你把Mock设置成“永远返回成功”那你在测试里就直接把结果写死了。如果某天支付网关真的挂了你的测试照样全绿它根本没有测到真实依赖的状态。所以Mock是工具不是免死金牌。它解决的问题是“如何让外部不稳定因素不再干扰我测试自己负责的代码”而不是“如何让测试看起来更漂亮”。用生活化的关系类比Mock像是剧组里的替身演员专挑高危动作或者特殊镜头时顶上替身怎么演、台词怎么说都由导演事先规定好真实主角不在现场。可要是连主角自己的面部表情特写都让替身来演最后成片的核心戏份就全变味了。很多测试项目正是因为把“主角”也替换成了替身导致测试跑得再绿也和真实故事没有关系。1.2 Spy装着摄像头的真演员Spy是在真实对象外面“包一层监控”。它不拦截真实逻辑真实代码该怎么走还是怎么走只是在关键调用点记录调用信息。你可以用Spy检查某个方法有没有被调用、被调用几次、传了什么参数。典型场景是在有历史包袱的遗留代码里——一个方法内部同时干了三件事读写数据库、调用外部接口、发送消息。加载它的依赖很贵你又不想把方法整个改写而是想观察某个内部方法是否按预期被依次调用、参数对不对。Spy恰好能胜任。继续用刚才的类比Spy不是替身而是站在真演员身边的场记。演员该演照样演但场记会记下每一段动作、对白、情绪变化。你不需要换掉演员你只需要知道演员有没有按剧本完成该有的动作。和Mock不同Spy默认会执行真实方法。这意味着如果真实方法里有数据库写入Spy也会一并执行。这一点既是Spy最大的价值也是使用它时最大的风险后文会专门展开。1.3 Mock、Spy、Stub、Fake的边界别再混着叫了很多人张嘴闭嘴“mock一下”实际用的可能是Stub或Fake。这种概念混乱在团队里特别常见而且很致命因为一旦说不清自己用的是什么出了问题根本没法定位。Stub是替代真实依赖的桩返回预设数据一般不校验调用关系它的存在是为了“有个东西能回答问题”。你传入一个测试数据它返回你预设的输出仅此而已。Fake是一个能工作的轻量级替身。比如用内存数据库代替真实数据库它实现了接口的所有方法但实现方式简单得多。Fake有真实逻辑只是简化版。Mock比Stub更进一步它不仅替身还负责验证交互——检查某个方法是否以特定参数被调用了正确次数。Mock天生自带“验证”属性这一点是它和Stub最大的区别。Spy则是在真实实现上增加验证能力逻辑还是那个逻辑只是多了一层记录仪。类型是否执行真实逻辑是否能验证调用典型用途Stub否否提供预设返回解决“依赖不存在”Fake是简化版否提供轻量级、可运行的替代实现Mock否是锁定外部依赖行为并验证交互Spy是是保留真实实现同时观测调用细节搞清楚这几种工具的区别你才能真正判断自己的测试里到底用了什么也才能接着往下聊“自欺欺人”这件事。2. 你的测试到底被什么“自欺欺人”了2.1 最常见的假隔离把很多公司真实的自动化测试拿过来看会发现一种很普遍的问题为了把单测跑起来把依赖全部mock掉结果所有Service之间直来直去真实协作完全消失了。表面上看确实隔离了依赖实际上只是把“你不愿意面对的依赖问题”藏了起来。举一个我实际改过的例子。测试一个“检查用户等级并发放优惠券”的方法原来的测试把所有数据访问全部mock只传入“等级为VIP”的结果然后断言优惠券发放逻辑正确。测试全绿但是你有没有想过“等级为VIP”这个结果从哪来底层用户表怎么组织、SQL怎么写你全都没管。如果数据访问层已经把“查询用户”的逻辑搞错这个测试会永远发现不了。这种测试写多了就会形成一种“我在隔离依赖”的错觉实际上隔离的是整个系统的数据流总和。测试跑得飞快但基本不判断真实业务是不是能工作只是在自己搭的玩具箱里玩积木。另外想说一句Mock也不只是代码里的对象行为替换。在接口测试和联调阶段用本地Mock Server、或者用Charles这类代理工具把前端请求转发到固定返回数据上也是常用的接口mock手段。这种mock隔离的是联调环境层面的依赖和单测里的对象mock目的相同但层次完全不同别混在一起讨论。2.2 过度Mock让测试失去保护价值过度Mock有三种常见表现。第一层层mock。一个请求从Controller进Service从Service进DAO每层都做mock调用链根本没有真实实现参与整个测试从头到尾是指挥假人走路。一旦真实逻辑里某个POJO字段改了名字测试完全看不出来因为它们用的mock根本不关心实体类字段怎么变化。第二mock的结果永远太过顺利。测试里只写成功路径从不模拟异常数据、空值、超时、重复提交。真要遇到生产事故那些“全绿”的测试帮不上任何一点忙。我见过一个服务上线前访问量猛增结果因为缓存穿透到了数据库数据库压力过大单测里缓存全部mock掉导致这种真实的依赖故障完全没有被预警到。第三断言只关注“某个方法被调用了”不关注调用参数、返回值、业务结果之间的关联。这样的测试本质上只是在确认“代码走到了这一行”而不是验证“代码产生了正确的业务行为”。测试一旦写到这个份上你对业务代码的保护能力基本为零。宁可少写几个精的测试也不要堆出几千个这种看不出问题的测试。测试的核心价值是变化发生时能提醒你哪里坏了而不是证明代码跑起来没报错。2.3 真正可靠的依赖隔离应该怎么设计不要把所有依赖都mock掉按层次来决定怎么处理。对于纯粹的内部业务对象尽量用真实实现。你创建订单时需要调用一个折扣计算器这个计算器本身逻辑复杂但它是项目里的纯函数没有网络、没有数据库完全可以真实调用它然后用测试数据去覆盖各种折扣组合。这样测出来的结果才是真正有用的。对于外部API、消息队列、短信服务、文件系统这些又慢又不稳定的东西果断mock。它们不在你控制范围内测试时容易出现“对方挂了等于我们挂”的假失败mock掉后测试稳定性会好很多。对于数据库访问优先用Fake用一个内存实现来替代真实数据库而不是mock每一行查询调用。这样你把整个数据层的行为保留了下来同时避免了环境依赖。对于“时间”“随机数”这类天然不可控因素用Mock去固定返回值和时间戳保证测试可重复。核心思路是mock的粒度应该控制在“外部不稳定因素”上而不是“你自己的所有业务逻辑”上。越接近核心业务越要让真实逻辑参与越接近外部边界越适合用替身。3. 实操拆解pytest和Mockito里的Mock与Spy3.1 Python pytest实战固定外部依赖的mock写法先说Mock。假设我有这样一段业务代码# order_service.py import payment_api def create_order(user_id, amount, currencyCNY): result payment_api.charge(user_id, amount, currencycurrency) if result[status] success: return {status: paid, order_id: generate_order_id()} return {status: failed, message: result.get(message, unknown)}测试时我不想真的发起支付请求于是把payment_api.charge这个外部函数mock掉# test_order_service.py from unittest.mock import Mock, patch from order_service import create_order patch(order_service.payment_api.charge) def test_create_order_success(mock_charge): mock_charge.return_value {status: success} order create_order(user_id1, amount100) assert order[status] paid mock_charge.assert_called_once_with(1, 100, currencyCNY)这个测试把外部依赖固定在“成功”状态验证了create_order的成功分支。它解决的问题是支付API不稳定时下单成功分支的逻辑仍能被快速回归。但请注意边界这是“固定外部行为”不是“验证支付逻辑”。一旦payment_api.charge函数内部逻辑写错比如真实接口要求amount必须传int而业务里传进来的是decimal测试完全发现不了因为mock直接返回了你想要的结果。如果我想保留支付接口的调用契约又想验证业务和支付之间的真实协作可以考虑用Mock(wraps...)来模拟Spy的效果from unittest.mock import Mock import payment_api def test_create_order_with_wrapped_spy(): # 给真实API包一层记录调用但不屏蔽真实实现 real_api payment_api.PaymentService() spy_charge Mock(wrapsreal_api.charge) order create_order_with_service(user_id1, amount100, paymentspy_charge) spy_charge.assert_called_once_with(1, 100, currencyCNY)wraps是Python的Mock里很容易被低估的一个参数。当你使用Mock(wrapsobj)时Mock不会屏蔽obj的方法而是调用obj的真实方法同时记录调用信息。这样你既保留了真实逻辑又能对调用进行验证这是“假”与“真”之间的一道重要分水岭。实际项目中更常见的写法是用pytest的mocker夹具配合patch来配置mock返回和验证调用。比如def test_order_failed_when_payment_network_error(mocker): mocker.patch(payment_api.charge, side_effectTimeoutError(gateway timeout)) with pytest.raises(TimeoutError): create_order(user_id1, amount100)side_effect用来模拟异常是mock的核心能力之一不仅限于正常返回值。写异常分支时别只测成功否则测试的保护作用会大打折扣。3.2 Java Mockito里的Mock和Spy以及那个坑Java生态里Mockito几乎就是测试的标准工具看两个实际例子。ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock PaymentGateway paymentGateway; Spy AuditService auditService; InjectMocks OrderService orderService; Test void testCreateOrder() { when(paymentGateway.charge(anyInt(), anyInt())) .thenReturn(new PaymentResult(true)); assertTrue(orderService.createOrder(100, 1).isPaid()); verify(paymentGateway).charge(100, 1); verify(auditService).recordLog(anyString()); assertEquals(CREATED, auditService.getLastStatus()); } }这里面Mock的paymentGateway完全替换掉真实网关因此当orderService调用charge时走的是替身返回预设的成功结果。这就是隔离外部依赖最直接的方式。Spy的auditService则不一样。它依然运行真实审计逻辑但测试可以观察它的记录。最后一行断言里我们验证了不仅recordLog被调用过而且真实状态被更新成了“CREATED”。这些业务状态只有真实逻辑执行了才会产生用纯Mock是保证不出来的。但是用Spy有一个特别容易踩的坑。如果你这样写when(spyObject.doSomething()).thenReturn(mock);Mockito为了实现这个stub会先真实执行一次doSomething()方法然后返回预设值。如果doSomething()里面有数据库写入、发消息等副作用测试运行时就悄悄执行了这些副作用非常危险。正确做法是doReturn(mock).when(spyObject).doSomething();doReturn().when()不会触发真实方法执行只对返回值做替换。这条经验我反复和团队新人强调因为真的有人在测试里因为用了when()导致测试期间往外发了一堆邮件。3.3 什么时候该用Mock什么时候该用Spy——我的选型清单场景推荐选择理由外部HTTP API调用Mock避免真实网络请求测试稳定可重复消息队列发送Mock避免在测试中产生真实消息投递贵重的数据库连接Fake优先必要时Mock保留数据层行为降低环境耦合自己项目里的纯业务计算逻辑真实对象用测试数据覆盖分支不需要替身遗留代码中需要验证内部方法调用次数Spy保留真实执行同时验证调用关系时间、随机数等不可控因子Mock固定返回值保证测试可重复我个人的经验法则是能用真实对象覆盖优先真实对象真实对象做不到再引入替身需要替身时优先选Fake或Spy只有外部不稳定依赖才用Mock。这个顺序基本能避免“过度mock导致测试失明”的老毛病。4. 自欺欺人的信号、排查技巧和我的避坑经验4.1 测试开始“变假”的四个信号有些信号一旦出现就是在提醒你测试已经开始自欺欺人了。第一改了方法签名、参数类型、返回结构之后相关测试居然还能全绿。正常来说你动了这些关键点涉及到的测试大概率要挂。如果完全不挂说明测试和真实代码之间基本没有连接。第二测试执行速度特别快覆盖率数字也好看但总感觉代码一上线就开始出问题。这通常是因为mock了太多东西真实的异常路径完全没被覆盖到。第三测试里有大片的Mock注解每个类上都是mock看起来隔离得很干净但测试主线里只有“自己”和“替身”没有“真实协作”。这种测试跑起来顺滑却很难在真实故障来临时给出任何信号。第四测试里出现大量verify(mock).method()的断言却几乎没有对返回值或数据状态的断言。这不是行为驱动而是“你告诉我调用了我怎么知道对不对”。4.2 快速排查对照表症状可能原因解决方式测试全绿但手动验证逻辑有问题mock范围过大把真正要测的逻辑也覆盖了缩小mock范围保留核心真实逻辑测试会随机闪挂不稳定mock没有正确复位上一个用例污染了状态使用clear_invocations/reset或在setup里重新创建用了Spy但出现了数据库写入、邮件发送when(spy.method())触发真实方法执行改用doReturn().when(spy)方式新增一个依赖后原有测试大面积崩新依赖没有被mock覆盖代码直接调用了真实依赖统计依赖图统一管理外部依赖的mock测试数量很多但覆盖率数据低测试大量重复mock相同对象结构沉重且断言过少提炼公共fixture增加行为断言mock能“验证”到内部调用但内部逻辑改动后测试依然通过mock设置和真实实现的绑定很弱比如anyString()尽量用贴近真实业务含义的具体参数做断言4.3 从坑里走出来的三条经验我实际参与过的项目里有一类特别典型Java后端覆盖率能到80%以上但是上了生产环境依然事故频发。后来逐层排查发现相当一部分单测从头到尾都在用mock把业务逻辑“架空”。这些测试表面上存在但写测试的人养成了一种习惯把所有可能干扰测试的东西全部mock掉却忘了问一句“我到底在验证什么”。后来我强制自己遵守三个原则。第一不能在没有真实对象参与的测试里断言业务状态。如果某一个断言必须依赖“真实逻辑执行后的结果”那么这个测试里就一定不能只用Mock至少要有一个真实对象、或被wraps包着的Spy参与。第二mock只用来处理外部依赖的不确定性不用来处理自己业务逻辑的复杂性。自己写的代码里如果有很多复杂分支应该靠数据驱动测试去覆盖而不是靠mock把分支抹平。第三每个测试写完后都反问自己我只测了这个分支如果前面的依赖出了别的问题会发生什么跑通和测透是两回事。让测试的“绿”建立在可重复、有保护意义的验证基础上比绿得一片祥和重要得多。最后再分享一个实用技巧。对于团队里的公共测试基类可以在里面统一管理外部依赖的mock比如Redis、MQ、OSS这些组件而在业务测试类里只保留真实服务和Fake依赖。这样既能保证单测的速度和稳定性又能让测试大概率覆盖到真实业务协作链路。我试过把一条核心链路的测试从“全Mock”改成“真实ServiceFake仓库”以后短短两周就捕获到了三次字段类型不匹配引发的潜在线上问题这比任何覆盖率数字都更能说明问题。
返回列表