[心得] 笑谈软件测试的几个阶段(六)历史代码

楼主: TonyQ (自立而后立人。)   2020-08-20 23:48:03
这系列最早是 2012 年的连载,时间过去八年,
最近常在想,一样是写程式,一样是会犯粗心的错。
为什么 coding 的速度也好,品质也好,
找问题的速度也好,不同人落差还是显著。
当年写这系列主要是要介绍开发测试的有趣之处。
有兴趣的可以 /笑谈软件测试 翻旧文。
======
今天要来继续往下写,当年也好,今日也好。
最多人问我的是,到底要怎么写啊,
真的要 unit 全面涵盖吗?
要怎么改 code 不用重写测试,很多很多的问题。
很多人也很常问,碰到一堆老 code 到底是要怎办。
里面有些是 IDE 能帮忙的,有些是自己的程式码组织模式问题。
我自己是不会太倚赖 IDE ,但也不至于不用 IDE ,
总之是人要比机体凶的那一派。
测资,测试模式,案例思维,可测试性在前几篇都讲过,
这篇要讲的两个点,一个叫"状态",一个叫"接口"。
在看历史代码时,大的原则是程式码不可能重头改完的,
所以问题是,你如何切割出真正的局部战场,并根据当前需要进入修正。
======
什么是状态(state),基本上就标准测试环节的术语,
mock 跟 stub 都某个程度上都在解决状态的问题。
状态意指著某个暂存一些变量或资料的载体,小从内存,大从 db / file / network 都算数
他的特色是,在你还没存取他之前,
他的值都有可能产生变化,并会因此导致测试的变动,
而让测试的价值产生变量。
(当然你跟我说档案、cache ,在你的情境是写死不会动的,
所以也是某种静态的资料,呃我不反对,但今天讲的是有机会动的情况。)
跟状态成对的是所谓的副作用,
指的是执行一段行为之后,
原本的作为环境资讯的状态被修改了。
像是你删除一则贴文,就找不到他了,这时候你就无法再对他做编辑的行为了。
这种状态变更就是一种副作用。
副作用一般来说是必然会发生的,但有几个大雷要避免,
一个是一般避免直接对传入的参数进行内容的变更,
真要变更还不如回传一个新的结果。
因为一般无法透过函式的存取接口,看见这个副作用的存在。
God object 会惹人厌,
主要一方面除了要准备这个物件的成本极高,
另一方面就是往往存在极为复杂的副作用。
所以当我们前提是在看历史代码时,
首先要留意的就是函式的分法跟副作用。
我们无法掌握前人的撰写风格或是程式技巧,
但小心的梳理副作用,让他在自己理解的范围。
必要时调整程式码来加强理解,
是阅读历史代码所必须掌握的技能。
另外状态的分立也是一个历史代码的观察点,
如果你发现同样的状态值,在多个 DTO 甚至 Entity 都存在,
那一般意味着这是个好用(参考价值高)的状态,
但同时也要留意对这个状态,各种计算是否有标准化,
如用 util 包装过或写成函式来操作。
以我现在的专案为例,人资系统除了真实时间以外,
往往都还有一个计薪日(又称归属日)的概念。
所以多数资料在处理发生时间时都会加计归属日。
那如何计算真实时间跟归属日的关系,
就应该有个有效的函式能够尽可能统一处理,
而不是每次都各显神通重新组装计算。
哪些要优先,哪些可以晚点再处理,在追踪代码时,
要有意识的去看哪些东西被引用的多,哪些东西被引用的少。
在心里面对所有的 entity/dto ,建立危险性跟重要性的判断。
然后,那些 common util 当然也是该优先涵盖的区域。
另外很多人谈状态,谈的是单一类别或两层内的状态。
但真正麻烦的 state 是要看整个 request life cycle 的 call stack ,
特别是这年头 closure / map & reduce 的写法盛行。
很多参数虽然是 local variable ,实质上却会传递的非常深入,
也往往会是很多操作转介的核心资料。
上游资料变化对下游结果非常敏感,
这种时候如果没仔细想清楚往往会有非预期的副作用出现。
如何适当的中断这么多层的 parameter 传递,
让状态减少到人脑可以思考跟负担的层次,就是一个重要的问题。
我理想中的状态比较像是递回那种做法,
每一层都只负责自己需要知道的,
但有更外层会收集子函式的结果来做该做的结果。
(这里也搭配 tell, dont ask 开发精神。)
======
再来是 interface ,
历史代码有时候难免会出现意大利面。
这时候要搭配前面的概念,根据手上所允许的时间,
在目标区域自己要修或观察的核心程式码,
将其转至为一个简单的 input output 无状态的映射。
这讲起来很抽象,比方说算上下班打卡是不是该打卡这件事情好了。
这个问题总共有几个变量,
一个是他有没有设定要求打卡提醒的参数,
一个是他当天有没有请假,
一个是他当天有没有出差,
最后一个是他有没有补登打卡的记录。
在我这的实作这四个本来都是捞资料,
所以可能就是类似这样:
function 大执行函式(){
var 打卡异常提醒 = false;
var 设定资料 = dao.读db设定();
var 请假资料 = dao.读db请假();
var 出差资料 = dao.读db出差();
var 补登资料 = dao.读db补登();
if (.....){
/*判断A*/
}
if (.....){
/*判断B*/
}
if (.....){
/*判断C*/
}
}
可能一大个函式就长这样,
但如果你想把这段程式转为可测试性,
很多人直觉会是要去让 DB 传回该传回的值。
更有概念一点的可能会想去做 dao 的 mock ,
我们这里很好心的是假设是同一个 dao,
现实世界可能还跨 db context or service 。
但就我自己的习惯是会这样改
function 大执行函式(){
var 设定资料 = dao.读db设定();
var 请假资料 = dao.读db请假();
var 出差资料 = dao.读db出差();
var 补登资料 = dao.读db补登();
return 检查资料(设定资料,请假资料,出差资料,补登资料);
}
function 检查资料(设定资料,请假资料,出差资料,补登资料){
if (.....){
/*判断A*/
}
if (.....){
/*判断B*/
}
if (.....){
/*判断C*/
}
}
主要的目的是把 db 的操作,
尽可能的在不浪费的前提下往前提,
并且将程式小心的区隔出无状态跟有状态的区域。
(有状态的区域就是一种,
比单一类别再大一点的状态容器。)
并且尽量把自己的目标区域,
设定成相对无状态的模式,并加以测试涵盖。
(逻辑相对稳定跟容易锁定)
======
有些人可能在这时候很快就会提出质疑,
一个是函式很肥很长不好切。
呃,这个问题的答案是,没人叫你切那么大,
正常情况下都是小规模堆叠,
如果堆几次堆不出新的公约数来简化查询。
那就是复杂度本身就是复杂到需要这样,
这时候只能跟着复杂度一起,
切出多区的状态容器跟无状态处理机。
另一个问题是,即使我们可以保护好无状态的处理机,
(特别留意,这里的保护好,同时包括掌握住所有可能的副作用。)
可能有些人习惯还是看最后的结果是 yes 还 no ,
会来质疑说,如果这四个数据库查询有一个错怎么办。
概念上还是得写四个数据库,查询跟检查的排列组合,
如果你这么想,那你这里就是进入了思维误区。
我只需要分开再写四组 test,
涵盖确保这四个 db 查询如预期就好,
虽然这里是带状态的,test case 不好写,
但不好写不代表做不到。
其次,我只要资料准备正确,
就能保证后面的无状态处理机在[已知的案例],
前提下的行为都是稳定可靠的。
这点对于我们抽离状态带来的不确定性,
已经产生了很大的帮助。
另外,这种接口抽换的技巧,在重构时也非常有用。
在历史代码复杂且手上规格参考价值不高的时候,
如果是为了解决逻辑层的计算问题。
我会先切割出小型的区域,然后复制一份旧的无状态函式版本保留作为对照组,然后将无状态函式重写。
使用旧的资料提供者并作 snapshot ,
将这些资料腾到测试案例,接上新的计算器,
确保新旧两个测试案例结果一致。
(若不一致不用惊慌,拿出规格跟资料, 找 user 确定谁是对的,
就我经验,重写很有可能找出陈年或前人逃避的问题。)
如果是效能问题,我通常是会重写资料准备区域,并且用同样的计算结果作为依据。
当然有时候会面临要改善必须两边一起动的情况,
这种选择我会放在最后面选,
同时因为能够对照的路牌比前两者少,
我会安排更多时间跟案例做测试验证。
不管是多少的程式码,
解决问题都只能从最小的单位一路向上堆叠。
很多人谈到历史程式码往往视为是负债,
这篇介绍的做法,本质上是在让你有机会借由前人代码撑竿跳。
达到同时确认品质跟思路,也不让自己暴露在太大风险之中。
具体在执行的时候都非常简单,
但心法却不容易掌握跟理解。
有兴趣可以从这角度想想看。
作者: aa08666 (Heaven)   2020-08-20 23:56:00
推一个在看
作者: vi000246 (Vi)   2020-08-20 23:59:00
先推再看
作者: black209 (black209)   2020-08-21 00:32:00
好文推
作者: APTON (玮玮)   2020-08-21 01:15:00
最近一个人默默写测试有感,心法真的要自己下去做才能慢慢体会
作者: anandydy529 (AndyAWD)   2020-08-21 01:56:00
作者: magus (Magus)   2020-08-21 02:42:00
推按错orz待会补推补推啦
作者: smart0eddie (smart0eddie)   2020-08-21 02:49:00
作者: atpx (秋雨的心情)   2020-08-21 03:21:00
感谢分享, 这些千锤百炼的内功真的要有一定经历才能体会
作者: leo770429 (leo)   2020-08-21 06:46:00
作者: popmentos (汽水 + 曼陀珠)   2020-08-21 09:21:00
感谢
作者: jixiang   2020-08-21 09:58:00
作者: ian90911 (xopowo)   2020-08-21 11:16:00
感谢分享
作者: liukuai (白日梦)   2020-08-21 11:48:00
推,感谢分享
作者: bigshawn (棉花)   2020-08-21 12:42:00
作者: zxcv860513   2020-08-21 12:51:00
作者: jj0321 (JJ与你倒数唷)   2020-08-21 13:21:00
push
作者: bnd0327 (阿噗噗)   2020-08-21 14:17:00
推推
作者: Diawchen (weiwei)   2020-08-21 14:56:00
作者: milkstrong (ToFu)   2020-08-21 17:00:00
作者: kyukyu (QQ)   2020-08-21 17:33:00
作者: onegoman (SKY)   2020-08-21 21:52:00
作者: jl40 (jl)   2020-08-21 23:56:00
测试真的是无底洞 没测试重构很有压力
作者: es8603 (绯色之翼)   2020-08-22 08:13:00
作者: TAKADO (朕没给的你不能抢)   2020-08-23 08:50:00
作者: mybluesky (天族)   2020-09-05 18:29:00
推,感谢分享

Links booklink

Contact Us: admin [ a t ] ucptt.com