2.2 关起门来测:隔离外部依赖


本节摘要:让单元测试"快稳自动"的抓手,是把外部依赖从被测代码里分离出去。核心手法是依赖注入——把依赖当作参数传进来,而不是在函数里直接 new。本节照一个真实改造案例,给你一套从"测不了"到"测得了"的标准工序。

一个测不动的函数

假设线上有个计费函数,它的实现里直接联网去扣款:

import requests def charge(user_id, amount): resp = requests.post("https://billing.internal/charge", json={"user": user_id, "amount": amount}) if resp.status_code == 200: return "ok" return "fail"

你要给这个函数写单元测试,瞬间撞上三堵墙:第一,真实网络调用让测试变慢且需要外部队列可达;第二,计费系统是生产级外部服务,测试里真扣钱谁也赔不起;第三,返回值直接取决于第三方状态,你无法稳定控制。这三堵墙不是函数的错,是"依赖被写死"的错——new 和直接连外部,等于把测试挡在门外

依赖注入:把墙变成插口

解法朴素却极其通用:别让 charge 自己造这个外部客户端,把客户端作为参数传进来。调用方(生产代码或测试)决定塞真货还是假货。这是所谓"依赖注入"的本质——不是框架的魔法,就一句话:依赖通过参数进来,不在内部用 new 生成

改造后:

def charge(user_id, amount, billing): resp = billing.charge(user_id, amount) # billing 是注入的端口 return "ok" if resp.status_code == 200 else "fail"

现在 charge 不再关心 billing 是真是假,它只管自己那点逻辑。生产环境传一个真客户端,测试传一个能指定返回值的假客户端,同一套逻辑两端皆可验。

把墙换成插口的完整过程

下面展开一次完整改造,走"背景 → 操作 → 结果 → 解读 → 变式"五步,你照着做就能迁移到自己的项目。

背景:仓储类 ikea_order 内部直接 new 了一个数据库连接器,读订单写订单都卡在这。

操作:给类增加可选的构造参数,默认仍走真连接,但允许注入替代端口;同时把"读订单"的实现剥成纯接口调用。

结果:生产代码几乎不改(只是多了一个可注入参数),但测试代码可以从真实的数据库里解脱出来,用内存假件顶替。

解读:改动很小,收益却大——顺序、时延、环境全都不再影响测试,快稳自动三条铁律一次达成。

变式:如果依赖是隐藏的全局变量(比如某个单例),就先把它改成函数参数;如果是 static 方法,就抽成实例方法再注入。总之动作都是一个字:把依赖从"内部够到的角落"挪到"从门外递进来"。

最小可用示例

不引入任何框架,光用 Python 的直接传参就能验隔离后的逻辑:

class FakeBilling: def charge(self, user_id, amount): return SimpleNamespace(status_code=400) # 可控地返回个失败 def test_charge_fails_when_billing_rejects(): assert charge(7, 9.99, FakeBilling()) == "fail"

断开网络后这测试照跑,返回值 100% 可预期,这就是隔离的报酬。真实项目里,端口常常长成一个接口、一两个方法,再加一层依赖注入容器把它接回生产路径。

隔离到什么程度才够

不是所有外部依赖都要机械地换成假件。现实的取舍标准:快吗(慢就换)?稳吗(会闪红就换)?贵吗(碰真实费钱或有副作用,如扣款、发信,就换)?三者都"否"的依赖,比如一个只读的本地配置,保留真货也说得过去。所以"关起门"的度是按成本和安全逐项算的,不是一刀切全替身。想要更细的替身分类和选择流程,下一节测试替身实验室接着讲。

⚠️ 常见坑:依赖注入很容易做过头,为了所谓的"纯净"把两层调用之间塞满抽象的端口,让测试像猜谜。记住目标是"必要处隔离",不是"处处建接口"。能用一个简单函数参数解决的问题,别建一个五层继承。

隔离的三种注入面孔

依赖注入在落地时长成三副面孔,选哪副取决于依赖的生命周期。第一副是"构造注入"——依赖经构造函数传入,适合一个类长期持有的依赖(仓库、服务、客户端),也最常用。第二副是"方法注入"——依赖经具体方法参数传入,适合只在某一次调用时需要、不必长期持有的依赖(比如某次请求里临时要的解析器)。第三副是"属性注入"——依赖经可写属性/字段塞进去,灵活但容易让依赖"隐身",可测性反而变差,一般只在测试配置里用。

这里给条选择口诀:依赖要跨多次调用共享,用构造注入;依赖只在一次调用用得上,用方法注入;能不用属性注入就别用——它最容易让人忘了依赖存在,从而把被测逻辑悄悄变成"看不见的硬编码依赖"。

一个防止"过度注入"的护栏

注入做过头和不足一样是坑。判断的护栏有三问:这个依赖是不是真的让测试变慢、变不稳、变贵(快/稳/贵三问)?三问都否,就保留真货;三问中确有,才值得注入端口。第二个护栏问:注入会不会改变被测行为的语义?如果只是把"两种环境下的同一套规则"换端口,语义没变,注入是干净的;如果把规则本身也替换掉了,那你可能已经在测假世界。第三问:有没有更简单的写法?能用函数参数、就别上类级抽象;能用默认参数、就别加容器。这三问能帮你不断把注入打磨到"必要的那个度"。

隔离问题 vs 设计问题的分辨

现实里"测不动"常常同时是隔离问题和设计问题混在一起。分辨方法是看——把依赖注入进去之后,剩下的被测逻辑是否足够短、足够直白。若注入后发现被测函数还是一团五层嵌套的临时变量和副作用,那说明不止缺注入,还缺"拆分":把大的副作用过程和纯计算核分开,让纯核单元测、边界动作交给集成。这一步把第 2.2 节和第 5.3 节的可测试性设计连在了一起,也正是后续第 5 章讲 TDD 时"先拆到可测"的伏笔。

四点快速自查

  • 被测函数里有没有"藏在内部够到的外部对象"——有,就把它提到参数位。
  • 生产代码调用与被测代码的差异,是不是只剩"传谁进来"——是,注入有效。
  • 能否仅靠换注入,就轻松产生成功/失败/异常三条路径——能,说明隔离到位。
  • 改测试时会不会被外部世界的时延或状态牵连——不会,隔离成功。

本节要点回顾

  • 写死即拦截:内部 new 外部 = 把测试挡在门外;注入端口 = 把门打开。
  • 本质一句话:依赖经参数进来,不在函数里 new 生成。
  • 五步改造:背景→操作→结果→解读→变式,动手照着搬。
  • 隔离按账算:快?稳?贵?三项都不中,保留真货也合理。
  • 别做过头:恰如其分地隔离,别为"纯净"建一堆抽象迷宫。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U