写测试代码这种事情 ,以前只在网上和书上看到过, 自己从来没有写过。 每当看到那些世界顶级程序员编写的技术书籍中出现“测试用例”“测试代码”的字样或者一些行业的鼎鼎大名的技术大牛们提及写测试的重要性的时候,我的心里就会产生一种自己编的一定是假程的错觉, 为什么我写代码就从来不用那玩意?

 

就拿开发一个MVC框架的Web应用程序设来说, 通常的做法就是新建一个控制器和一个模型, 把代码要实现功能的业务逻辑写在模型里面,控制器调用模型, 假如有外部参数则接收参数传递给模型, 假如业务逻辑过于复杂导致模型过于臃肿或逻辑不顺畅, 则再进行梳理或提取,构建成一个新类,再由模型进行调用。 这一过程反复循环迭代, 直到功能开发完毕。 调试或者测试写的代码是否能得出想要的结果, 自然也是使用最简单粗暴的方法, 在浏览器中运行程序, 定位到控制器, 控制器调用模型, 模型再调用其它所涉及到的类,拿到结果后再一步步返回, 浏览器是否显示预期结果就意味着我们写的程序是否正确。不管是我们要测试的功能模块离控制器只有一个调用还是有十个调用,都遵循着这样一个步骤, 因为这是最符合我们直觉和习惯的方式。 我也一直以这种方式在开发程序。

 

原本这也没有什么问题,我们所写的代码逻辑是通过我们的大脑深思熟虑组织后产生的,通常情况下我们有这个把握可以确定代码逻辑运行的正确性,就算出现意料之外的情况, 多点几下浏览器的刷新按扭也能把问题找出来解决,因为我们对代码的运行逻辑了然于胸,自信不会出什么叉子,一旦出现了叉子那就产生了所谓的程序BUG。

 

然而, 万事总有例外, 导致我们以往的经验失效。 就拿我最近碰到的一件事情来说,公司有一个项目因性能优化需要,对部分功能进行技术方案调整, 重写了代码。代码量不大, 功能本身的代码和其依赖的通用函数代码加起来一共也就二三百行,但是隐含在背后的逻辑却异常复杂,涉及到的数据表也有五张。我将这部分需要重写的代码重头至尾仔仔细细读了一遍, 勉强能理解每一个语句块都干了些什么。 可能是我逻辑思维能力不过关, 也有可能是代码太过于复杂 , 我没有办法将所有这些代码的来龙去脉全盘了然于胸,也就没有办法从全局的角度去梳理代码逻辑确定优化方案,我只能从局部的角度出发, 依样画葫芦的按照旧方案重新实现一遍代码的逻辑, 在实现的过程中如发现有优化的余地则进行局部优化,等到足够熟悉全局逻辑后,再从宏观的角度对代码结构进行调整优化,这么做效率是低了点,却是最保险的做法。

 

我照着旧代码写出一个个一模一样的函数,却没有办法确定这些函数的运行结果是否能得出预期的结果,鬼知道换一种语言实现以后, 函数吐出来的结果还是不是和之前的一样,我可没有jeff dean那样牛逼,预判代码的结果比编译器还精准。本来这也不是什么大问题,把代码跑一遍,当执行到这些函数的调用时自然就知道结果了。问题出在这之中某些函数和代码的入口隔着七八个调用,而且其中某些调用因为依赖于某些if条件判断结果而不是必然被调用到的,要构造出能使这些函数被调用到的if条件判断分支走向的参数环境是一件异常繁琐的事情,光想想就让人觉得烦躁和气馁。另一种方法就是把函数的调用代码复制一份放到执行入口的开始位置,这样代码一运行就直接能调用的到了。 然而, 这种方法也会带来问题,如

 

函数处于不同的类和包内,调用函数需要导入包和实例化类,而做这些事情对项目的本身没有实际的意义

 

某几个函数只在所在的类内被调用, 访问修饰是private, 通过这种方法测试它的准确性还需要放开权限把访问修饰声明为public, 调试完毕后还得改回去, 操蛋

 

有多个分布在不同类之中的不同函数需要以类似的方式测试, 反复进行这些无意义且繁琐的操作, 极度浪费时间,影响心情

 

代码的执行入口总放着那么一坨被注释掉的代码,想拿掉又怕拿掉以后下次还要用, 内心挣扎的难受

 

因此, 想要解决这个问题, 上面的两种方案都不可取, 柔肠百转也想不出像样的解决方案。 长辈们都说编程都是脑力劳力, 我之前不以为然, 但当碰到这些想破脑袋也找不到办法的问题时就不得不承认, 编程的确是脑力劳力。我才20岁,外表却有30岁可以看,我想也跟长期被这些问题困扰有一定的关系(我说的是10年前的自己)

 

我思前想后,检索所有脑子中关于程序设计的资源, 才找出一个之前从来没有尝试过的方案, 引入单元测试。我这个人有一个优点, 在工作上碰到陌生的东西从来不会望而却步,只要有用处, 都会去积极尝试。对于单元测试,我虽然没有掌握使用的方法, 但是网上查查资料, 看看教程, 我相信花不了多少功夫就能搞出来。 事实也的确如此, 只看了一篇资料,照着教程的步骤操作就把测试程序跑起来了。 我使用的是go语言, 按照go test的规则 ,被测试的代码所在的文件名加上test后缀即可作为测试代码所在的文件的命名,如下图

测试函数的命名方式必须要以Test作为前缀, 如下图

测试代码编写完成后, 在代码所在的文件目录下使用cmd运行go test命令,测试代码就可被运行了

需要测试的函数在测试代码中被直接调用, 省去了跟踪庞杂代码执行走向的麻烦,从复杂的业务逻辑中解放出来, 非常的清晰方便。

 

从表面上看, 写测试代码的好处就是方便测试函数的正确性, 然而, 随着之后代码的编写, 我发现写测试代码所带来的好处不止于此。当有了要为代码编写测试用例的前提条件后, 我在实现某个函数时就约束自己, 这个函数必须要方便编写相应的测试代码。有了这层约束以后, 我发现写出来的代码的质量要比不写测试用例时高, 比如

 

函数的功能职责更加单一了,换言之, 函数的逻辑更稳定了, 不易产生变动, 因为我不想我辛苦编写的测试代码随着函数的代码的调整而付之一炬。

 

不会很随意的把代码乱放, 写出来的代码更加整洁,该提取函数时就建新函数, 该内联函数时则删除不必要的函数,在之前, 为了偷懒往往会对这些细节视而不见, 这会加速代码的腐烂。

 

更早的发现BUG,很多时候, 程序的BUG都是在生产环境中由用户发现,原因很简单, 开发项目的速度和质量这对冤家之间程序员往往会选择前者,此外, 程序员会毫无根据的信任自己写的代码,因此当向程序员反馈BUG时,他们都会保持怀疑的态度。很多时候, 程序员写一个函数通常只给一个特定的输入,运行后发现输出如自己预期那样后就默认这个函数是健康的, 事实上, 当给这个函数另外的输入时, 函数吐出的结果就在预期范围之外, 这便导致了BUG的产生, 个中原因便是对自己直觉盲目的信任, 认为自己的大脑就是一个人肉编译器。 编写测试可以很大程度上的杜绝这类问题

 

通常,我们会认为编写测试是一件浪费时间的事情, 然后就是一边向别人吹牛一边则啪啪啪的打自己脸。 除此之此, 在开发项目时常常以逻辑不稳定随时需要调整代码为理由拒绝写测试,然而, 当从相反的方向来考虑问题时会发现, 有了测试的约束后,我们会更加仔细和严谨去编写每一个函数 ,逼迫自己更加深入的考虑问题而防止代码走样, 提高代码质量、安全性以及稳定性, 这也是写测试所带来的至关重要的意义。