1. 先把话说清楚:Harness到底在测什么、装了哪些部件
在自动化测试这个圈子里,我经常看到两种完全相反的状态:一种是团队写了三五十个零散的测试脚本,谁也没法统一维护,改了接口就全线飘红;另一种是团队买来或抄来一堆测试框架,配置了洋洋洒洒几百行,最后跑起来却连基本的环境隔离都没做。这两种状态的病根,其实都在同一个词上:测试Harness。很多人以为Harness就是“测试框架”,是某个现成的测试库,装上就能用。但真正做过可靠自动化的人会告诉你,Harness是介于测试框架和被测系统之间的那一层“装配层”,它要管的是用例怎么被找到、数据怎么被造出来、外部依赖怎么被隔离、失败之后怎么被复现。
这篇文章要解决的就是一个核心问题:怎么从零构建一套真正可复用、可维护的Harness,并且把其中的六大组件一个个拆开讲透。适合谁来读?正在写接口测试或者服务端测试但总觉得脚本像一锅粥的开发者;想从手工验证往自动化测试跨一步但是被各种框架文档绕晕的测试工程师;以及那些已经在用现成测试库,却说不清楚用例为什么时灵时不灵的团队技术负责人。如果你是这类人,建议把这篇看完,我会用一份贯穿全文的模拟项目X来演示整套逻辑。项目X是一个订单支付子系统,后端暴露了下单、查单、退款、对账四个接口,足够把六大组件全部用上。
1.1 测试Harness不是测试框架
为了不把概念搞混,我先做一个明确的区分:测试框架负责的是“用什么样的语法写测试”“怎么运行测试”“怎么收集结果”这类通用能力;Harness则是基于框架之上,针对你的被测系统做的一层定制化装配。说得直白一点,框架是发动机,Harness是你把发动机装进这辆具体车型时需要的那些支架、线束、冷却管路。你换一辆车,发动机还是那个发动机,但线束和支架一定不一样。这也是为什么很多团队直接拿框架当Harness用,结果总在环境切换、数据清理、依赖替换这些问题上反复折腾,因为框架本身就不会替你考虑“被测系统长什么样”。
真正意义上的Harness,需要有明确的输入和输出:输入是被测系统的版本、测试目标、环境分支、数据种子;输出是翔实的测试报告、可复现的失败现场、稳定的通过与否判断。它不是一个库,而是一套你亲手搭出来的装配逻辑。六大组件就是这套装配逻辑里最核心的六块积木。
1.2 为什么是六个,不是四个也不是八个
我见过一些团队把Harness拆成三个部分:执行、校验、报告。我也见过拆成八个部分的,连“日志采集”“性能采样”都单独算一个组件。三个太粗,八个太碎。我倾向于稳定的六分法,原因很简单:你沿着一次测试的执行路径走一遍,数一下必须要经历的动作,就会得到这六个。
先从一堆源码里找出哪些是测试用例,这是一件事;把选中的用例加载起来并排定运行顺序,这是一件事;给用例准备它需要的测试数据,这是一件事;用例真正跑起来之后,外部依赖不能真的去连生产或者测试环境的第三方,要用替身顶住,这是一件事;跑完之后拿实际结果跟期望值做比较,这是一件事;最后把失败信息、堆栈、上下文整理成可诊断的报告,这又是一件。合起来刚好是六件,对应的就是用例定义与发现、测试执行器、测试数据工厂、测试替身、断言校验器、报告与诊断器。再往后还能拆出日志收集、环境准备、覆盖率采集,那都属于“增强件”,不是“基础件”。基础件缺一个,Harness就像缺了一块支架的发动机舱,看着能用,颠簸一下就散。
1.3 贯穿全文的模拟项目X:一个订单支付子系统
为了让六大组件不是停留在口号上,我在下文会一直拿模拟项目X举例。项目X的业务逻辑很简单:用户下单后,系统先调用支付通道接口,支付成功则生成支付单并异步通知回调;查询订单时返回订单状态;退款接口则先校验支付单是否存在,再调用支付通道的退款接口;对账接口按时段拉取支付流水。整套服务粗看只有四个接口,但这里头有三个共性问题:数据依赖订单号、支付流水号和三方通道状态;外部依赖是支付通道和异步消息队列;失败类型有超时、金额不一致、状态流转非法。
这些问题如果不用Harness管起来,写测试的时候就会陷入“造一个订单都要十几行代码”“随时可能真的调用到外部支付通道”的泥潭。接下来的六大组件,全都围绕项目X展开。这个例子虽然小,但它覆盖了大多数服务端测试的典型难点,你把它们换成用户系统、内容审核系统、物流系统,逻辑一样成立。
2. 六大组件全景:谁负责跑、谁负责比、谁负责作证
在逐个深入之前,我先把六大组件放进一张表里。这张表的作用不是让你背概念,而是为后面所有讨论提供一幅“装配图”。我后面讲的每一段,你都可以回来对照这张表看位置。
| 组件 | 核心职责 | 一句话比喻 |
|---|---|---|
| 用例定义与发现 | 描述“测什么”、自动识别哪些函数是可执行用例 | 给每发炮弹写好目标参数 |
| 测试执行器 | 调度用例顺序、控制并发、处理超时与重试 | 火炮手,负责按射击口令开炮 |
| 测试数据工厂 | 构造被测系统需要的订单、用户、流水等数据 | 弹药车间,按需冲压标准零件 |
| 测试替身 | 在测试中用可控替身替换支付通道、消息队列等外部依赖 | 演习对手,演什么像什么 |
| 断言校验器 | 对结果做精确比较,生成能定位问题的差异描述 | 裁判,只说是或不是,不白说 |
| 报告与诊断器 | 汇总结果、留存失败现场、输出可复现步骤 | 黑匣子,出事时必须能还原现场 |
看到这里,你可能觉得这些组件也不稀奇。确实,稀奇的不是组件名字,而是它们之间怎么咬合。比如:执行器需要从用例定义模块拿到用例列表,拿到之后又要去问数据工厂“这个用例绑定了哪些数据模板”,数据工厂返回的数据里如果带了三方依赖,执行器再通知测试替身挂载对应的Mock行为。这一整条链路不是靠框架自动完成的,而是靠你在Harness里把它们接起来。
具体到项目X,一次下单成功用例的完整流程是这样的:发现器从测试目录里扫出“当支付通道返回成功时,下单接口应返回支付成功”这个用例;执行器从用例定义中解析出它需要一条“通道成功”的支付流水作为种子;数据工厂按照模板生成一个订单号和一笔金额;替身模块给支付通道接口注册一个返回成功的桩响应;断言器最后校验返回值中的支付状态码等于200。任何一个环节掉链子,这个用例都会失败,而且失败的根因可能根本不在业务代码里,而在你的Harness装配上。这也是为什么我把Harness判定为“四两拨千斤”的高杠杆投资:架子搭对了,之后每加一条用例的边际成本都极低。
3. 执行与定义:Runner和用例模型是怎么咬合的
3.1 用例模型:把测试写成数据和代码的混合体
先讲用例定义,是因为它是整套Harness的“第一步”。项目X里我常用的做法是:不直接写一个又一个裸函数当作用例,而是定义统一的用例对象。它至少包含四样东西:唯一标识、用例名称、前置条件(依赖的数据模板和环境要求)、执行体(真正的测试步骤函数)。
很多刚接触的人会问:这不是多此一举吗?直接写函数然后加个装饰器不行吗?我的回答是:直接写函数当然可以,但你把用例的“描述信息”和“执行逻辑”绑死之后,报告模块就没法拿到结构化数据,执行器也没法根据前置条件调度用例。举个例子,项目X里“测试退款接口在支付失败时的表现”和“测试退款接口在支付成功后退款超时的表现”,如果都用裸函数写,你很难在报告里快速筛选出“哪些用例依赖退款超时模板”。一旦把用例抽象成对象,前置条件变成字段,报告模块自然就可以按条件筛选,数据工厂也能按模板批量生成数据。这是整个Harness具备可观测性的地基。
实际定义用例时,我习惯把用例模型拆成两层:描述层和执行层。描述层用声明式写法,比如指定数据模板标识、指定替身行为;执行层才写具体的请求、断言。好处是同一个执行体可以配合多套数据模板,跑出多组变体,这也是参数化测试的本质。项目X里的支付成功回调测试,我就是用同一套执行体配了“金额正常”“金额尾数带角分”“金额为零”三套数据模板,跑出三个独立用例,报告里一目了然。
3.2 Runner:顺序、并行、超时与重试
执行器是整个Harness里最容易被低估的组件。很多人觉得Runner就是“把用例一个个跑完”,其实没那么简单。一个真正可靠的Runner至少要回答四个问题:这些用例能以什么顺序跑、能不能并行跑、单个用例被卡住怎么办、失败了要不要重试。
先说顺序。项目X里有一组用例有严格顺序要求:必须先创建支付单,才能去测试支付回调;必须先有支付成功的订单,才能测试退款。我见过很多团队把这种顺序依赖直接写在用例内部,于是出现了“用例A调用用例B”的脏代码,跑起来看着没问题,一换执行顺序就全挂。正确的做法是把顺序依赖表达在用例模型的依赖字段里,让Runner自己拓扑排序。比如B的依赖字段写了A,Runner就必须先跑A、A通过才跑B、A失败则B直接跳过,而不是硬跑之后看到一个莫名其妙的失败。
再看并行。并行不是越猛越好,而是要分“安全级”。项目X里,查询类用例完全不依赖数据库写状态,全公司项目都可以随意并行;但退款类用例会修改支付单状态,同一个订单号并行跑就会互相踩。所以我的Runner每一次执行都需要一个“并行度”参数和一组“资源锁”——按订单号哈希分片加锁,保证同一个订单上的用例串行,不同订单之间并行。这套逻辑听起来繁琐,但它是实现大型测试套件时间可控的唯一出路。
超时和重试也要讲策略。超时的作用不是捕获“慢”的用例,而是捕获“挂死”的用例。项目X的三方支付通道替身偶尔会模拟长时间不响应,这时候如果Runner没有超时设置,一个用例可以卡半小时。合理的超时应当按用例类型分别配置,而不是全局一刀切。重试的使用则要克制:只有明确的“外部瞬时抖动”才适合无脑重试,业务断言失败重试十次还是失败,重试只是掩盖问题。我的原则是:替身或环境相关的偶发问题,最多重试两次;业务断言失败,一次就够。
3.3 用例发现机制:约定优于配置,但要有边界
用例发现要解决的是“不用每次手动注册用例”。如果每写一个用例都要去中央清单里登记,肯定会漏,尤其在人多事杂的团队里。比较成熟的方案是约定目录加标记:测试目录下的所有文件,只要包含特定后缀或被特定装饰器标注的类/方法,就自动被收集为用例。
但约定一定得有边界。团队里有人图省事,把工具类也扔进测试目录,于是发现器把一堆非用例对象当成用例收进去,轻则报错,重则把数据准备函数当成测试跑了。所以我会给发现器加两道门槛:第一道,文件名必须匹配明确模式;第二道,函数或方法必须带有某个统一标记属性,比如在类上标了test_case或在函数身上标了@test装饰器。两道门槛都过了才算用例。项目X里有一个专门放通用步骤的包,不会触发发现器扫描,因为路径直接被排除了。这样既能保持“新增用例零配置”,又能避免把非用例误收进来。这个排他逻辑看起来很简单,但在真实的Harness中,最多的误收集风险恰恰来自那些“看起来像测试的辅助模块”。
4. 数据与方法:测试数据工厂与断言校验层
4.1 测试数据工厂:从硬编码到对象母体
第二个在项目X里血泪最多的组件是测试数据工厂。拿订单系统举例,一个正常订单需要订单号、用户ID、商品清单、金额、状态、创建时间、关联的支付流水。新手喜欢在每个用例里手写一段字典,复制粘贴到五六个地方,等哪天下单逻辑加了“渠道来源”字段,整层用例都要重筛。这种硬编码数据的维护成本,是拖垮测试代码的头号元凶。
我推荐的演进路线是“对象母体”模式:先定义一份最小可用的默认订单数据,这个默认数据能通过所有基础校验;然后为每种测试场景定义一个“变异方法”,只覆盖你关心的部分。项目X里我维护了一个订单母体,比如基础状态是“已下单未支付”,金额是100.00元,用户ID是固定测试用户。变异方法比如with_paid()会把状态改成已支付,with_amount(999.99)会覆盖金额。这样一来,每一条用例里看到的数据定义都是“从母体来,再改两个字段”,读起来像白话,也能躲开批量数据维护的坑。
数据工厂还需要考虑“数据的生命周期”。项目X的测试数据分三类:一类是外部平台固有数据,比如支付通道的测试商户号,这类数据应与环境绑定并放在配置文件里;第二类是每条用例临时生成的数据,比如订单号、支付流水号,这类必须保证唯一性,我习惯用时间和随机数拼接,同时要防止并行执行时的碰撞;第三类是批量种子数据,比如对账接口需要某个时段内一百笔流水,这类数据要能批量生成,生成后要记录量级和统计摘要。一个合格的数据工厂,需要让用例作者只看得到他关心的一小部分数据,把“这个订单号哪里来的”“这笔钱到底打没打”这类问题全部交给Harness去管。
4.2 断言库不是简单的if:失败信息的艺术
断言可能是看起来最简单的组件,无非是assert x == y。但在我做过的大型测试系统里,最影响排查效率的恰恰是断言写得太糙。一个典型的反面教材是:assert resp.status == 200,当这个断言失败时,它只会告诉你“False is not true”,具体是返回了500还是404,一概不知。这种断言等于没做。一个好的断言,失败信息应该直接给出“期望值、实际值、差异点、相关上下文”。如果响应体是一个JSON对象,断言还应该指出具体是哪个字段对不上,是值不对还是字段缺失。
项目X里退款接口出现过一次经典事故:退款金额被序列化成浮点数,0.1 + 0.2的结果在JSON里显示为0.30000000000000004。如果用简单的相等断言,用例必挂,但这不是业务错误,只是精度比较方式不对。这提醒了我:断言策略必须和数据类型绑定。金额这类精确数值,比较时要用最小精度单位转成整数再比;时间戳要统一精度;字符串要注意前后空格。断言库的职责不是替代产品经理定业务规则,而是忠实地把“两层结果不一致”这件事以最小噪音暴露出来。
再往深走一步,一个成熟的Harness还应该有“复合断言”和“集合断言”。项目X对账接口的核心断言是“对账单中的流水集合与本地支付流水集合一致”。这个用例如果用循环逐条比较,失败时很难看出是多了哪条还是少了哪条。所以我会在断言层提供集合差集计算,失败报告里直接列出“本地有但对账报告缺失的三笔流水号”“对账报告有但本地缺失的两笔流水号”。这样定位问题的时间从几小时压缩到了几分钟。
4.3 黄金文件测试与模糊校验
还有一类断言值得单独说:黄金文件测试。它的本质是拿当前真实输出和一份预先审定的标准输出做对比。项目X的回调通知报文,字段特别多,逐项用传统断言写下去会非常冗长,而且每次新增字段都要改一堆断言。我用黄金文件存储一份“本次回调报文的期望完整样例”,测试时把实际报文做同样的格式化后与黄金文件对照。这个办法减少了很多重复劳动,但有一个容忍度问题:报文中有些字段是时间戳、随机请求号,每次客观上都不同,直接全文比对会误伤。
我的做法是对这份黄金文件做“模板化”:把客观动态字段替换成{{timestamp}}这类占位符;对比时先把实际报文中的对应字段替换回同一占位符,再做全量比对。这样既保持了“全字段校验”的强度,又不会因为动态字段导致假失败。模糊校验则是另一类需求,比如金额总和要在一定容差范围内,数组中全字段匹配的比例要超过95%。这类校验不该放在用例代码里,而应该沉淀成断言库里的通用工具,方便所有测试复用。断言组件的终极目标,是让测试作者能写出“一眼能看出业务意图”的校验语句,而不是一大坨循环加if。
5. 隔离与证明:测试替身与报告诊断
5.1 五个替身角色的边界:Dummy、Stub、Spy、Mock、Fake
测试替身是Harness里最容易借用术语混乱的部分。我经常在一个团队里听到有人把所有替身都叫Mock,最后你根本分不清他是要拦截方法、返固定值,还是要记录调用次数。我建议把替身按角色严格区分成五种:Dummy只用来占位,比如参数需要填一个对象但永远不被真正使用;Stub用来制造“确定的间接输入”,比如支付通道总是返回成功;Spy用来记录“被测对象调了什么参数”,比如回调通知是否被触达;Mock则带预期行为,比如“必须被调用一次,否则失败”;Fake是真实组件的轻量替代实现,比如用内存版消息队列代替真实的中间件。
这个区分不是学术洁癖,而是直接关系到Harness的行为可预测性。项目X里有一个经典的费解场景:测试“支付成功回调只推送一次消息”,有人用了一个无限宽容的Stub去替代消息队列,这当然测不出来“重复推送”问题;有人又用了过于严格的Mock,要求消息队列“刚好被调用一次”,结果因为业务代码在异常路径里重试了一次,本来正确的行为被判失败。正确的做法是具体问题配具体角色:验证结果用Stub,验证交互用Spy,验证调用约束用Mock。角色用混了,替身给你提供的信息就是错的,比没有替身还可怕。
5.2 什么时候该上替身,什么时候别上
这是一个比“怎么用替身”更值钱的问题。我的判断标准可以浓缩成三个词:可靠、可控、可观察。如果真实组件在测试环境里本身就很可靠,比如只读又不落库的查询服务,就别上替身;如果真实组件会受时间、网络、第三方状态影响,比如支付通道、短信通道,就必须上替身。可控是指你需要在测试中制造“超时”“返回成功但回调延迟”“返回金额不一致”等场景,真实组件根本演不了这些,而替身可以;可观察是指需要确认被测代码是否真的调了某个依赖,真实组件很难给你反馈。
项目X中的支付通道是必须替身的典型:你总不能真的去调第三方支付通道,也不能让通道在测试环境里随缘返回失败。我搭的替身允许你通过一个简单配置来指定本次返回状态码、响应延迟、响应报文格式。这样一条测试用例就能覆盖“支付通道超时3秒后客户端是否自动重试”“通道返回签名字段缺失时订单是否被标记失败”这类业务故障场景。消息队列同理,我用内存队列替身替代真实中间件,既能模拟“投递成功”“投递失败”“投递后延迟消费”三种情况,又不会依赖外部服务存活状态。
至于Fake,是我在所有替身里最谨慎使用的。Fake写起来像真实组件,但它毕竟不是原版,一旦它的内部行为与真实组件有偏差,测试就可能“假绿”,意思是你以为逻辑没问题,其实只是Fake放过了它。所以我通常只在依赖组件非常稳定、接口定义明确且实现简单时才用Fake,项目X里的时间服务被替换成一个可拨动的时钟Fake,我会在Harness里加一条“冒烟测试”,专门验证“Fake的所有方法都被调用过且没有抛异常”,尽量降低假绿概率。
5.3 报告必须解决的问题:失败发生时怎么在三分钟内定位到代码
报告模块是整个Harness面向使用者的窗口。一个失败产生后,测试人员最想知道三件事:是哪条用例挂了,挂在了哪个步骤,挂的直接原因和间接原因是什么。很多框架默认报告只会给你“断言失败”和一行堆栈,这在用例少的时候还能忍,用例一旦到几百上千条,工作效率会被严重拖垮。
我在项目X的Harness报告里至少会输出六类信息:本次运行的任务ID、被测系统版本与提交号;用例标识、名称、执行状态、耗时;失败断言的具体差异,包括字段路径、期望值、实际值;完整请求与响应报文,做脱敏处理;所挂载替身的行为配置;以及测试数据工厂生成的数据摘要。最后一类尤其关键,因为很多失败只有拿到“喂给被测系统的数据长什么样”才能复现。我见过太多只有一句“订单创建失败”的报告,给不出当时传入的订单号,开发只能去日志里大海捞针。
报告同时还要做到“失败可复现”。我现在会在Harness里把每次失败用例的完整上下文序列化成一份JSON现场快照,存到测试输出的指定目录下。出现可疑失败时,可以直接用一条命令重放这份快照,连数据母体和替身配置都不用重新猜。实测下来,这个功能让项目X里“偶发失败”的排查时间从过去的小半天压缩到了几次重放就能定位。诊断能力是报告组件的分内之事,不能靠事后猜。
6. 从零到可维护:最小Harness骨架与我的演进顺序
6.1 一个直接能跑的最小骨架
前面讲了这么多原理,最后还是要落到代码上。下面这个简化骨架,是以项目X为例的最小Harness。它可能不是最优解,但它把六大组件的边界展示得非常清楚,你可以在它基础上继续填充:
# harness_core.py # 组件1:用例定义与发现 ------------------------------------------------- class TestCase: def __init__(self, case_id, name, data_template, func, deps=None): self.case_id = case_id self.name = name self.data_template = data_template or {} self.func = func self.deps = deps or [] self.status = "pending" self.detail = "" def discover_cases(root_dir): # 约定:文件名以 test_ 开头,方法标有 marker cases = [] for path in root_dir.rglob("test_*.py"): mod = load_module(path) for attr in vars(mod).values(): if hasattr(attr, "harness_case"): item = attr.harness_case cases.append(TestCase(item["id"], item["name"], item["data_template"], attr, item.get("deps", []))) return cases # 组件2:测试执行器 ------------------------------------------------------ class HarnessRunner: def __init__(self, data_factory, double_manager, reporter): self.data_factory = data_factory self.double_manager = double_manager self.reporter = reporter def run_case(self, case): case.status = "running" data = self.data_factory.build(case.data_template) if "payment_channel" in case.data_template: self.double_manager.mount_stub("payment_channel", case.data_template["payment_channel"]) try: case.func(data) case.status = "passed" except Exception as exc: case.status = "failed" case.detail = repr(exc) finally: self.double_manager.unmount_all() self.reporter.record(case) def run_all(self, root_dir): cases = discover_cases(root_dir) for c in cases: self.run_case(c) self.reporter.summary()这个骨架把用例发现、执行器、数据工厂接口、替身管理器、报告器都串联了起来,但没有把数据工厂和替身写死。实际项目中,我会把数据工厂替换为真正的订单母体,替身管理器替换为支付通道替身注册表。你从这行代码开始读,就能看出每一个组件在哪里接进来、在哪里被调用。
6.2 六大组件的推荐搭建顺序
如果你是从零开始,我强烈建议按下面的顺序分四步去搭,而不是一次性六件套全上。第一步,先搭用例定义与发现、测试执行器、报告与诊断。这三样是骨架中的骨架,哪怕数据工厂先写死,替身先不做,只要跑起来能看到一条用例失败并留下可读报告,你的维护成本已经从零迈出了一大步。第二步,接上测试数据工厂,把散落各地的硬编码数据收拢成母体。这一步会让用例数量快速增长时不失控。第三步,再上测试替身,重点处理外部依赖。这一步把测试环境.GetComponent 稳定性提上来。第四步才去优化断言库和诊断深度,比如加入集合断言、黄金文件对比、失败现场快照。
为什么要最后做断言和诊断?因为它们是最容易“过度设计”的部分。如果前三个组件还没稳定,你花很大精力做的精细断言和豪华报告,很容易因为用例本身跑不起来而白费。反过来,等骨架稳定了,再根据实际失败模式去补断言工具,每一件都会有明确的使用场景,不会制造一大堆没人用的抽象。我见过太多人第一周就写了精美的数据工厂,结果连一条用例都跑不通,这就是顺序反了。
6.3 三个最容易翻车的点:共享状态、Mock漂移、断言吞掉根因
最后分享三个我在项目X里真实踩过的坑,希望你别再走一遍。第一个是共享状态。测试用例之间如果共享了同一个订单号或者同一个静态变量,并行执行时就会互相污染。我的解法很土但很有效:所有订单号、用户ID、支付流水号都强制从数据工厂生成,生成逻辑里带上用例ID和随机数,不允许在用例里手写常量订单号。代价是你没法在测试代码里一眼看出“这笔钱就是100块”,但换来的是并行执行的安全,这笔账划算。
第二个坑是Mock漂移。所谓Mock漂移,是指替身录制的行为与被测系统当前的真实接口不再匹配。支付通道接口从v1升级到v2,字段变了,但替身还停留在v1,于是所有“成功路径”的用例全部通过,实际上被测代码里对接v2的改动根本没走通。这也是我坚持在Harness里加入“替身接口契约校验”的原因:每次挂载替身之前,自动对替身方法签名和真实客户端使用的第三方SDK作匹配校验,不一致直接判定失败。这能提前暴露“业务改了但替身没改”的问题。
第三个坑是断言把根因吞了。项目X早期有段测试代码是这样写的:整段业务调用包在一个try/except里,断言失败后捕获异常只打印一句“结果不对”。结果线下排查时永远不知道具体是哪一步错了。我后来把Harness的Runner设计成不拦截任何断言异常,让异常沿着调用链完整抛上来,报告模块再捕获原始堆栈并补充上下文。这样做的直觉是:Harness允许失败,但不允许“失败得不明不白”。这个原则值得刻在测试代码文件的开头注释里。
最后说一句
我在实际项目中搭过很多次Harness,最深的体会是:Harness的复杂度不会消失,只会转移。你不用Harness管数据,数据就会散在用例里;你不用替身管外部依赖,外部依赖就会在半夜给你发告警;你不做失败现场快照,定位问题的时间就会成倍膨胀。把六大组件按正确的顺序一个个接好之后,后续每增加一条用例的成本会低到让人忘记这项投资的存在。希望你搭出来的Harness,也能让团队专注于业务本身,而不是整天跟测试基建搏斗。