分布式程序的自动化回归测试

发表于:2011-5-05 10:59

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:陈硕    来源:51Testing软件测试网采编

  本文所谈的“测试”全部指的是“开发者测试/developer testing”,由程序员自己来做,不是由 QA 团队进行的系统测试。这两种测试各有各的用途,不能相互替代。

  我在《朴实的C++设计》一文中谈到“为了确保正确性,我们另外用Java写了一个测试夹具(test harness)来测试我们这个C++程序。这个测试夹具模拟了所有与我们这个C++程序打交道的其他程序,能够测试各种正常或异常的情况。基本上任何代码改动和bug修复都在这个夹具中有体现。如果要新加一个功能,会有对应的测试用例来验证其行为。如果发现了一个bug,先往夹具里加一个或几个能复现bug的测试用例,然后修复代码,让测试通过。我们积累了几百个测试用例,这些用例表示了我们对程序行为的预期,是一份可以运行的文档。每次代码改动提交之前,我们都会执行一遍测试,以防低级错误发生。”

  今天把 test harness 这个做法仔细说一说。

  自动化测试的必要性

  我想自动化测试的必要性无需赘言,自动化测试是 absolutely good stuff。

  基本上,要是没有自动化的测试,我是不敢改产品代码的(“改”包括添加新功能和重构)。自动化测试的作用是把程序已经实现的 features 以 test case 的形式固化下来,将来任何代码改动如果破坏了现有的功能需求就会触发测试 failure。好比 DNA 双链的互补关系,这种互补结构对保持生物遗传的稳定有重要作用。类似的,自动化测试与被测程序的互补结构对保持系统的功能稳定有重要作用。

  单元测试的能与不能

  一提到自动化测试,我猜很多人想到的是单元测试(unit testing)。单元测试确实有很大的用处,对于解决某一类型的问题很有帮助。粗略地说,单元测试主要用于测试一个函数、一个 class 或者相关的几个 classes。

  最典型的是测试纯函数,比如计算个人所得税的函数,输出是“起征点、扣除五险一金之后的应纳税所得额、税率表”,输出是应该缴的个税。又比如我在《〈程序中的日期与时间〉第一章 日期计算》中用单元测试来验证 Julian day number 算法的正确性。再比如我在《“过家家”版的移动离线计费系统实现》和《模拟银行窗口排队叫号系统的运作》中用单元测试来检查程序运行的结果是否符合预期。(最后这个或许不是严格意义上的单元测试,更像是验收测试。)

  为了能用单元测试,主代码有时候需要做一些改动。这对 Java 通常不构成问题(反正都编译成 jar 文件,在运行的时候指定 entry point)。对于 C++,一个程序只能有一个 main() 入口点,要采用单元测试的话,需要把功能代码(被测对象)做成一个 library,然后让单元测试代码(包含 main() 函数)link 到这个 library 上;当然,为了正常启动程序,我们还需要写一个普通的 main(),并 link 到这个 library 上。

  单元测试的缺点

  根据我的个人经验,我发现单元测试有以下缺点。

  ● 阻碍大型重构。

  单元测试是白盒测试,测试代码直接调用被测代码,测试代码与被测代码紧耦合。从理论上说,“测试”应该只关心被测代码实现的功能,不用管它是如何实现的(包括它提供什么样的函数调用接口)。比方说,以前面的个税计算器函数为例,作为使用者,我们只关心它算的结果是否正确。但是,如果要写单元测试,测试代码必须调用被测代码,那么测试代码必须要知道个税计算器的 package、class、method name、parameter list、return type 等等信息,还要知道如何构造这个 class。以上任何一点改动都会造成测试失败(编译就不通过)。

  在添加新功能的时候,我们常会重构已有的代码,在保持原有功能的情况下让代码的“形状”更适合实现新的需求。一旦修改原有的代码,单元测试就可能编译不过:比如给成员函数或构造函数添加一个参数,或者把成员函数从一个 class 移到另一个 class。对于 Java,这个问题还比较好解决,因为 IDE 的重构功能很强,能自动找到 references,并修改之。

  对于 C++,这个问题更为严重,因为一改功能代码的接口,单元测试就编译不过了,而 C++ 通常没有自动重构工具(语法太复杂,语意太微妙)可以帮我们,都得手动来。要么每改动一点功能代码就修复单元测试,让编译通过;要么留着单元测试编译不通过,先把功能代码改成我们想要的样子,再来统一修复单元测试。

  这两种做法都有困难,前者,C++ 编译缓慢,如果每改动一点就修复单元测试,一天下来也前进不了几步,很多时间浪费在等待编译上;后者,问题更严重,单元测试与被测代码的互补性是保证程序功能稳定的关键,如果大幅修改功能代码的同时又大幅修改了单元测试,那么如何保证前后的单元测试的效果(测试点)不变?如果单元测试自身的代码发生了改动,如何保证它测试结果的有效性?会不会某个手误让功能代码和单元测试犯了相同的错误,负负得正,测试还是绿的,但是实际功能已经亮了红灯?难道我们要为单元测试写单元测试吗?

  有时候,我们需要重新设计并重写某个程序(有可能换用另一种语言)。这时候旧代码中的单元测试完全作废了(代码结构发生巨大改变,甚至连编程语言都换了),其中包含的宝贵的业务知识也付之东流,岂不可惜?

  ● 为了方便测试而施行依赖注入,破坏代码的整体性。

  为了让代码具有“可测试性”,我们常会使用依赖注入技术,这么做的好处据说是“解耦”(其实,有人一句话道破真相:但凡你在某个地方切断联系,那么你必然会在另一个地方重新产生联系),坏处就是割裂了代码的逻辑:单看一块代码不知道它是干嘛的,它依赖的对象不知道在哪儿创建的,如果一个 interface 有多个实现,不到运行的时候不知道用的是哪个实现。(动态绑定的初衷就是如此,想来读过“以面向对象思想实现”的代码的人都明白我在说什么。)

  以《Muduo 网络编程示例之二:Boost.Asio 的聊天服务器》中出现的聊天服务器 ChatServer 为例,ChatServer 直接使用了 muduo::net::TcpServer 和 muduo::net::TcpConnection 来处理网络连接并收发数据,这个设计简单直接。如果要为 ChatServer 写单元测试,那么首先它肯定不能在构造函数里初始化 TcpServer 了。

51/512345>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号