作者langrisser19 (lan)
看板Soft_Job
标题Re: [讨论] 工作上写单元测试的比例
时间Thu May 2 15:27:35 2024
因为大家的讨论都很基於心法
实作上相对很模糊
利用这个机会也跟大家请教实作上的方式
因为最近工作被指派要针对公司产品的程式做整理,其实运作都还好
只是大家功能是一层叠一层,一堆巢状逻辑,跟依赖中的依赖
也没有任何的测试跟注解,当然也没有任何测试
举个例子
有个功能是储值,可以接受银行、信用卡、或是一个临时帐号
然後储值前需要身分验证,然後银行的话要检查是不是有绑定
总之每个方式都有一些共用,或是非共用的行为
目前的程式像这样
func 储值(方式){
switch 方式{
case 方式1:
if 符合条件1 {
if 符合条件2 {
if 呼叫api-1 成功{
更新介面1
}
}
}
case 方式2:
要符合不同的巢状条件,然後呼叫另一只api,一样根据结果更新不同的介面
case 方式3:
又是不同的条件跟api
}
}
像这样的程式,不知道测试怎麽写?
单看这个函式的cyclomatic complexity,大概是20几吧
如果把测试写成涵盖所有的可能
像是=>
当选择方式1储值,且不符合条件1,就要如何如何
整个测试就写不完了
而且条件本身又依赖於其他的api或是系统参数
就算写完了测试,涵盖率虽然高,但我觉得是没有解决核心的问题
我就只是要确认我程式本身的逻辑没问题,不需要涵盖别人的东西
这边我实在很好奇大家的作法是怎样?
是跳过不做,还是真的就写一个涵盖所有可能的测试
我的选择是先重构啦
先定义一个抽象的RechargeCommand介面
包含执行储值操作所需的方法
其中execute()会统一回传一个Result物件,代表储值结果
接下来为每种储值方式实作具体的命令
例如BankRechargeCommand、CreditCardRechargeCommand和AutoRechargeCommand等
然後一些通用的行为,像是登入验证、密码输入等,用装饰者模式
定义一个RechargeCommandDecorator抽象类别,继承自RechargeCommand介面
最後为每种需要共用的行为创建一个具体的装饰者,继承自RechargeCommandDecorator
例如
LoginCheckDecorator和BioAuthDecorator
呼叫的时候,根据需要去组合不同的储值方式,跟需要的共同行为
回传值也都是相同的Result物件
呼叫会像这样(我是ios swift)
let bankRechargeCommand: RechargeCommand = BankRechargeCommand()
let loginCheckDecorator = LoginCheckDecorator(bankRechargeCommand)
let bioAuthDecorator = BioAuthDecorator(loginCheckDecorator)
let result = bioAuthDecorator.execute()
呼叫api的部分用策略模式做依赖注入
在command实作中,接收一个RechargeAPIService,预设是真实呼叫
做单元测试的时候就单独传入mock service
测试也改成针对不同的储值方式做测试
而不是去涵盖所有的可能路径
这样就可以大幅减少测试案例的撰写,也可以节省复制贴上的时间
我只要确定我的内部逻辑正确,理论上不管怎样的路径应该都不会出错
也不会被绑定在外部依赖
整个流程大概就是把一个巢状判断函式
更改为 command+decorator 模式,根据需要呼叫与包装对应的行为
然後外部的依赖透过注入,在测试时使用mock避免被干扰
也请大家看一下
不知道这样的修改是不是可以
还是应该以涵盖所有的可能状况为优先 @@?
※ 引述《TonyQ (得理饶人)》之铭言:
: 先说我不是故意要回两篇,
: 但刚看到 landlord (就 joey chen, 江湖名 91) 在 FB 的回应,我觉得也蛮好的,
: 他说他最近在忙没空过来,我问过他之後帮他转过来。
: 以下基本上逐字照转
: (source from https://tinyurl.com/rxyerfyw )
: 其实讲到底根本原因反而是跟产品程式码的设计能力有关,
: 产品程式设计得越好,测试程式越容易写,越好测。
: 真正需要在测试中做假模拟(隔离)的部分,
: 属於外部(拥有权不在我们手上的部分),
: 例如外部系统的服务(走通讯协定出去,且不属於我们可以维护跟上版的服务)、
: 三方(package/SDK)。而 DB, redis之类的 cache 甚至是不需要特别被隔离开的。
: 这是由於现代科技的便利,让我们有机会把越来越接近端到端测试的一类,
: 比例逐步拉高的可行性比过去容易得多。
: 另一个重点则是当设计越来越偏向高内聚,simple design,
: 把 code smell 消除到最後回很自然地提炼出 domain model,
: 有了 domain model,
: 最复杂的 domain logic 处理一堆散落资料的逻辑都被内聚到model里面,
: 没有 application 层的依赖,model 的单元测试也很好写。
: 结论:
: 1. 要有能力在 legacy 上重构出可测试性
: 2. 要有能力做出稳定的端到端测试
: 3. 要能精炼设计,将散落的资料内聚在一起
: 来代表 domain 的概念提供 domain 的行为,
: 因为设计上本来就没有外层的依赖,model的方法也都精简短小,甚至鲜少回传值,
: 自然 API 易用性跟测试都可以比过去万恶的三层式架构+内嵌无限层依赖注入
: 的手风琴架构来得简单跟好测许多。
: 现在大部分的依赖(注入)都不是本质上需要的,而是被开发人员硬生生切得支离破碎的。
: 补充一下 TonyQ 内文最後一点:
: 「如果都没被报 bug,你也没有修改它的需要时,帮它加测试干嘛?」
: 这超级重要的,这种情况下加测试往往适得其反,
: 只会建立伪阳性/阴性的测试结果,劳民伤财还造成干扰。
: ※ 引述《TonyQ (得理饶人)》之铭言:
: : 底下这是比较「野性」」的作法,算是实务专案的经验:
: : 其实我觉得你到一个完全没有测试的专案,要分两个策略:
: : 1. 补重要主线的 integration test 反正哪边常被报修就补哪边。
: : 如果一开始补不上去就先做下一点,理论上常被报修的地方会一直出现在下一点,
: : 累积多了就可以变成1了。
: : 2. 假设自己是维护历史功能,提昇自己维护部分的可测试性。
: : 举实际案例好了,假设我今天再做一个算金流手续费的专案,
: : 发现过去算手续费假设有11个地方写了11次好了,所谓的高耦合不外乎如此。
: : 我会先写个 util 把输入跟输出「去状态化」,然後针对这个 util 写,
: : 然後这个 util 的单位以「去状态化」成本可控,可在手边开发时间允许的范围进行。
: : 白话文:我横竖都得手动测试,那就把手动测试的部分,
: : 尽可能的透过 test code 来进行。
: : 如果不想接着维护的话也很单纯,任务结束後把 test code comment 掉或移除就行。
: : 题外话,11个地方,我会选择先取代一个地方,
: : 然後等其他10个地方有需求变更时,一个个整并,补强测试条件。
: : 很多人会说,那为什麽不一次改11个,理由是你的开发时间跟成本允不允许。
: : 更重要的是你的QA资源够不够,因为写正常的Test最累的是准备测试情境,
: : 这真的是会花掉比写test更多的时间。
: : 如果列不出完整场景,贸然修改既有的code只是在裸奔。
: : 有需求的部分是被迫裸奔,但没需求的部分不用主动当暴露狂,
: : 等待验证过再慢慢统一。
: : 大原则就是:你横竖都是要测试的,只是手测还是写程式测,除了跟 gui 有关的东西,
: : 多数的情况下写程式测试都还是比较省时间的。
: : 更棒的地方是,在这种策略下,你往往可以用比同事少30% 的时间完成任务,
: : 而且因为你的测试成本比较低,所以品质也会比较好,出问题的时候也更容易对焦。
: : 然後我通常是会跟同事说我写了几个 test case,
: : 他们愿意看就看,不愿意看我就放着。不会勉强他们加入。
: : 如果你做不到可以用比不写测试更短的时间来完成任务,
: : 那你学的测试根本性的就有问题,不写也罢。XD
: : 3. 极端情况: 如果都没被报bug,需求也都很小?
: : 那你操心他干嘛 XD
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 59.120.34.91 (台湾)
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/Soft_Job/M.1714634857.A.75E.html
1F:→ landlord: 刚好之前写过类似的题目,有带到重构的过程跟影片 05/02 15:35
3F:→ landlord: 当时也有朋友支援各语言版本库,给大家参考一下 05/02 15:36
4F:→ landlord: 当然,写测试的话还是得知道有哪些情境,才能用测试描述 05/02 15:37
5F:→ landlord: 但这个重构的过程,即使没测试,有pair跟 code review 05/02 15:37
6F:→ landlord: 也不会有太大的成本跟风险问题 05/02 15:37
神...Orz
7F:→ brucetu: 你的最後一段假设就是在说如果单元测试都没问题整合测试 05/02 15:39
8F:→ brucetu: 跟上线理论上都不会爆掉 05/02 15:39
9F:→ brucetu: 实际上呢?如果这个理论正确那就不用写整合测试了 05/02 15:40
10F:→ brucetu: 所以要保证不爆掉当然需要所有的输入变数的可能性跟路径 05/02 15:41
11F:→ brucetu: 都测试过,符合预期,才有可能保证上线不爆掉 05/02 15:41
其实因为我的是app,所以其实我这段测试其实更偏向整合测试而不是单元测试
只是也很好奇
所谓的整合测试,在牵涉到外部的时候,需要一起测吗?
因为我的测试程式本身就包含了登入检查、生物辨识、呼叫api
只是这些部份我使用的是mock
这些物件的测试应该用其他的方式或包装,不应该影响储值这件事跟储值的测试
加上我回传的是一个通用的Result物件
这个Result要不是成功(包含成功储值的相关资讯),要不是失败(失败的相关资讯)
流程中如果任何一个环节失败了,对呼叫端来说都是收到失败的Result
这时候就是处理失败的介面与逻辑
原则上我还是会在测试中补上测失败的测试案例
但应该就是几个常见的,不会去涵盖每一个
我的目标还是很简单,我要确认的是程式这边的逻辑有没有问题
输入变数已经被command模式限制了,以这个案例来说,好像不知道还有啥变数
且不管哪个环节错,他就是抛Result<Error>
额外的测试,说真的,我觉得自己按一下比较快
理论上也不应该出现没捕捉到的错误
真的有...大概也是很稀少的案例,这个就交给内部测试或是实际使用者吧 冏rz
※ 编辑: langrisser19 (59.120.34.91 台湾), 05/02/2024 15:59:22
12F:→ TonyQ: "如果任何一个环节失败了,对呼叫端来说都是收到失败的" 05/02 16:00
13F:→ TonyQ: 这个可能 app 比较没感觉,但是监别不同的失败对debug重要 05/02 16:01
14F:→ TonyQ: 题外话你这个情境算是很特定的情境了,我自己在这个时候会 05/02 16:35
15F:→ TonyQ: 思考的问题是,「假设我的程式出错了,我能不能避开最大的 05/02 16:35
16F:→ TonyQ: 伤害。」 这也是一个可以考虑的事情。 05/02 16:36
17F:→ brucetu: 理论上牵涉到第三方服务的时候你要mock第三方服务 05/02 19:48
18F:→ brucetu: 实务上第三方有提供测试区的话我会直接开一个测试区db直 05/02 19:49
19F:→ brucetu: 接在上面测试 05/02 19:49
20F:推 wulouise: 写一个新的加测试 05/02 21:30
21F:推 OldTjikko: 巢状太多了,重构看看能不能guard clauses 05/03 00:21
22F:→ yamagishi: 太巢惹, ealry return 用一下该测的东西分一下你自然知 05/03 08:32
23F:→ yamagishi: 道 test 要怎麽写 05/03 08:32
24F:→ new122851: 当你发现UT写不下去时,你要的是重构 05/03 19:56
25F:→ rogerlarger: 把每个if内的code都分别做成function内,然後再写un 05/10 14:08
26F:→ rogerlarger: ittest? 05/10 14:08