写单元测试简直是傻,如果不符合预期,我就直接改我自己的代码好了。
如果说领导让研发写单元测试,我敢打赌80%的研发脑海里都会想过这个问题。 我写了一个函数,这个函数的结果我当然知道是什么,但是为什么我还要写一个单元测试来确定这个事情?
我的答案是,再简单的事情,都有可能出错。
让我们想想我们的工作中的情况在自己写功能的时候,有多少情况是自己一次编写就能确保这个函数能够一次编译通过的?随着自己工作年限的增长,我写的代码越多,就越明白一个道理: 人是会犯错误的,无论这个错误有多么低级。
以下是一个非常简单的例子。
class Member
def need_notication?
return false if %w(cancel paid).include? self.status
self.expire_date - 1.week > Time.now
end
end
如此简单的代码,程序员一眼就能看明白,那为什么我们要对如此简单的代码写单元测试呢?
有一天下午,我晕晕乎乎的读到这段代码的时候,觉得这里的 expiration 的判断有问题,于是乎,改为了
self.expiration_date + 1.week > Time.now
当时比较忙,觉得改动很简单,直接push代码了
由于早期写这个功能的时候顺手写了个简单的单元测试
it "could send expiration notification correctly" do
member = build(:member, expiration_date: 5.days.since)
assert_equal true, member.need_notication?
member2 = build(:member, expiration_date: 1.month.since)
assert_equal false, member.need_notication?
end
push代码后一两分钟,我们的CI系统 flow.ci 就发邮件提示单元测试失败了,打开邮件一看,原来是自己脑袋不清醒瞎改东西了。
会有人说,自己不会犯这个错误,其实我在清醒的时候也不会犯这种错误,但是我深知自己肯定会有不清醒的时候。
就像那句著名的概率学玩笑 “不可能事件随着时间的累积是一定会发生的” 一样,项目越大,越久,出现各种自己预想不到的错误越大。该大写的小写了、打字 "O" 结果按到 "0" 了等等问题,都会出现,更别代码提逻辑上的错误了。
当然,很多种方式可以避免这种问题。大多数人使用工手动测也能发现类似的问题:在代码中加 print, 或者在 IDE中debug自己刚写的代码,手动跑一跑就可以解决。
然而换个角度想想,为什么不能将“手动跑”这个过程代码化过来呢?我将手动测试转化为单元测试时间大概是原来手动测试的 2-3 倍。乍看起来还挺多的,然而却解决了我另外一个问题:无论什么时候,我都能确保我的函数是测试过的
一般在上线的时候大家都会问,“所有功能测过了么?” 如果这次上线是一次大上线,积累了很多功能,我相信没人能够说“都测过了”,就算都测过了,也会因为时间间隔太久,而不敢确定这件事情。但如果自己写过单元测试,则每次运行测试的时候,所有测试用例都会运行,会非常有底气的说,都测过了。
更重要的是,当项目的代码发生变更的时候,仅仅跑一下测试,就能重复之前的测试动作,判断修改的代码是否影响到了之前的逻辑。特别是当项目由很多人参与的时候,时常代码的逻辑就被你不知情的改变了,而跑一遍单元测试,如果代码的改变影响到了测试的逻辑,则测试就会failure,这时大家就可以充分的意识到修改的内容影响到了哪些代码了。
这一系列的好处,仅仅是比手动测试多出 2-3倍 的时间而已。远远低于在出问题后debug的时间。
很多时候,我们拒绝做出那些看起来无趣的事情,然而就是那些看起来很愚蠢,很繁杂的事情,却会在你不经意的时候,拯救于你。不信,你可以想想背包里的伞,以及钱包里的夹层。
确定引入单元测试后,肯定会有一些阵痛感,如何降低这些阵痛,提高单元测试的价值,下次写 “小谈单元测试写法”。