Re: [讨论] 工作上写单元测试的比例

楼主: langrisser19 (lan)   2024-05-02 15:27:35
因为大家的讨论都很基于心法
实作上相对很模糊
利用这个机会也跟大家请教实作上的方式
因为最近工作被指派要针对公司产品的程式做整理,其实运作都还好
只是大家功能是一层叠一层,一堆巢状逻辑,跟依赖中的依赖
也没有任何的测试跟注解,当然也没有任何测试
举个例子
有个功能是储值,可以接受银行、信用卡、或是一个临时帐号
然后储值前需要身分验证,然后银行的话要检查是不是有绑定
总之每个方式都有一些共用,或是非共用的行为
目前的程式像这样
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
作者: landlord (91)   2024-05-02 15:35:00
刚好之前写过类似的题目,有带到重构的过程跟影片https://bit.ly/4b0B3Nb当时也有朋友支援各语言版本库,给大家参考一下当然,写测试的话还是得知道有哪些情境,才能用测试描述但这个重构的过程,即使没测试,有pair跟 code review也不会有太大的成本跟风险问题
作者: brucetu (sec)   2024-05-02 15:39:00
你的最后一段假设就是在说如果单元测试都没问题整合测试跟上线理论上都不会爆掉实际上呢?如果这个理论正确那就不用写整合测试了所以要保证不爆掉当然需要所有的输入变量的可能性跟路径都测试过,符合预期,才有可能保证上线不爆掉
作者: TonyQ (自立而后立人。)   2024-05-02 16:00:00
"如果任何一个环节失败了,对呼叫端来说都是收到失败的"这个可能 app 比较没感觉,但是鉴别不同的失败对debug重要
作者: brucetu (sec)   2024-05-02 19:48:00
理论上牵涉到第三方服务的时候你要mock第三方服务实务上第三方有提供测试区的话我会直接开一个测试区db直接在上面测试
作者: wulouise (在线上!=在电脑前)   2024-05-02 21:30:00
写一个新的加测试
作者: OldTjikko (欧洲云杉)   2024-05-03 00:21:00
巢状太多了,重构看看能不能guard clauses
作者: yamagishi (山岸刑务官)   2024-05-03 08:32:00
太巢惹, ealry return 用一下该测的东西分一下你自然知道 test 要怎么写
作者: new122851 (未若柳絮因风起)   2024-05-03 19:56:00
当你发现UT写不下去时,你要的是重构
作者: rogerlarger (宅)   2024-05-10 14:08:00
把每个if内的code都分别做成function内,然后再写unittest?

Links booklink

Contact Us: admin [ a t ] ucptt.com