莫名其妙的故障
这周遇到了一件事,让我意识到了回归测试的重要性。
事情是这样的:上周团队中开发人员 A 和 B 各自完成了一个需求,经过 peer review 已经合并到 master 了。但因为当时已经是周四晚上,时间太晚,就没有上线。
周一下午我负责上线,打包 master 上传到服务器(两台)并重启服务(对!我们还没有自动部署!),测试人员完成了线上验收,一切正常。
第二天早上来到公司,还是一切正常。
到了 11 点,出问题了。
其实也不是什么非常严重的问题。我们有个聊天机器人,每天固定 11 点会在业务群里发送前一天的订单情况汇总,但今天这个消息没发出来。检查一番很快发现,相应的订单查询接口报错了。一同受到影响的还有后台管理系统的订单查询功能。
发现这个问题的时候几个相关人(包括我在内,因为前一天是我部署上线)正好都在开会,于是我们急忙回到工位开始排查。
一番检查之后,大家都舒了一口气,因为受影响的服务只限于内部系统,至少用户是无感知的。
但与此同时,这个故障又令人匪夷所思,因为前一天上线的功能里,没有任何对订单系统的修改啊!无论是内部系统的订单查询逻辑,还是订单创建、状态更新的逻辑,这次都没有任何改动。
为什么订单查询接口莫名其妙就报错了呢?
排查问题
既然是订单查询接口坏了,那自然要先找到相应的开发人员。这就是前面提到的开发人员 B。
B 是我们团队中唯一一个外包,他这人比较糊涂,按理来说是不应该在这儿的,但我们招人实在太难,不得已只好请外包来帮忙,平时也不太敢让他开发比较核心的功能。
由于 B 没有登录线上机器的权限,所以我来帮他查看线上日志。看过日志之后发现,直接原因很清楚,是一个空指针错误。
于是我们一起看报错的那一行代码,发现唯一可能出现空值的那个 DAO 属性,又恰恰是不应该出现空值的。因为那个属性是从数据库的一个字段通过 mybatis 映射过来的(重点,记住这里),而那个数据库字段又恰恰是 NOT NULL
的。
这就怪了。但这么干想是解决不了问题的,于是我从线上导出来前一天的订单记录,让 B 插入到测试库里(不然测试库里没订单),然后在本地打断点调试订单查询接口。
表层原因
毕竟这事也不太紧急(业务那边甚至没有过来责怪我们,因为他们正好在忙一件更重要的事情),后来我就干自己的事去了。
过了一阵,B 来找到我,说他找到原因了,但特别奇怪。
确实是我们怀疑的那个 DAO 属性出现了空值。那个属性叫做 order_status
,数据库里相应字段的名称也叫 order_status
- 这当然是一种不好的做法,因为 DAO 属性应该遵循驼峰命名规则,叫做 orderStatus
才对。
B 说,他把那个 DAO 属性名改成 orderStatus
,这个问题就不出现了。
这很好,但问题是之前一直是这么写的啊,为什么之前没问题,这次突然就出问题了呢?
B 百思不得其解,我因为也正在忙别的事,就让 B 赶紧先把 bug 修复了,深层原因之后有空再找。
意想不到的深层原因
后来我就把这事忘了,在自己的分支上继续开发。
晚上,我的分支基本开发完毕,准备把 master 上最新的内容合进来,然后提测了。
合并 master 的具体内容我一般是不看的,但这次正好遇到了冲突,在解决冲突的时候我就看到了所有在合并中会被更新的文件列表。
然后我发现 application.yml
文件也被更新了。这个文件是很少有改动的,出于好奇我就看了一下有什么改动,即便这些改动实际上并没有和我的分支冲突。
这一看不要紧,我发现 application.yml
中新增了如下两行配置:
mybatis:
mapper-locations: classpath:mapper/**/*.xml
# 下面这两行是新加的
configuration:
map-underscore-to-camel-case: true
愣了几秒之后,我恍然大悟。
所谓 underscore,就是 order_status
这样的下划线命名规范;所谓 camel case,就是 orderStatus
这样的驼峰命名规范。
新增这条配置之后,mybatis 会把数据库字段 order_status
自动映射为 orderStatus
,但 B 的 DAO 属性却还是 order_status
。拿不到映射过来的值,当然就会出现空值了!
这两行代码的提交时间是 5 天前,但昨天(周一)才上线,于是今天(周二)就发现问题了(如果没有那个定时聊天机器人,可能发现得更晚)。
问题是,这两行代码是谁提的呢?
不是 B,而是 A。
B 对此一无所知。
而 A 显然也想象不到 B 竟然会蠢到不用驼峰规范来命名 DAO 属性。
实实在在的教训:回归测试的重要性
说了这么多,其实这次经历之所以给我深刻的印象,就是因为这是我第一次实实在在地遇到了一个回归测试本可以避免的 bug。
之前在一个比较大的互联网公司做过 QA 实习,当时每次代码改动都要从我们这里跑回归测试,通过了才能合并、上线;上线之后自然也是要对线上服务跑一遍回归。
然而我在的那三个多月里,从来没听说回归测试帮助发现了什么真正的问题,倒是经常出现假警报,白白耗费精力。假警报的原因翻来覆去就是那么几种:
- 测试机器 IP 漂移了,导致所有回归测试全挂掉;
- 模型有变化,导致之前应该返回某个值,现在返回另一个值了,但测试用例没有更新;
- …
后来来到这个项目组,做前端开发又转成服务端开发,期间也没遇到什么回归测试能派上用场的地方。原因很简单,就那么两三个人,谁在动哪块的代码彼此都知道,而且本来就是各自负责一部分逻辑,很少会有交集。
现在项目组逐渐壮大起来了,服务端开发从最初的一个人变成了 5 个人加一个技术 leader,但服务还没完成拆分,所有系统都在一个单体服务里。在这种情况下,这个问题终归是出现了。
我们其实也并非没有回归测试,但测试只覆盖了一小部分最核心的代码。这也无可厚非,毕竟现在业务刚起步,迭代速度这么快,今天写的测试可能明天就没用了。如果要保证测试覆盖全部代码,恐怕测试和开发得是接近 1 :1 的比例吧。显然有这些预算还不如多招几个开发,现在开发都不够用呢;等服务稳定下来,再逐渐增加测试覆盖度。
这估计也是为什么很多庞大的系统都有“遗产代码”的原因吧,因为初期实现全量测试覆盖的成本太高了。