单元测试(Unit Testing) – 对已有代码添加单元测试

来源:互联网 发布:ltd域名后缀不能备案 编辑:程序博客网 时间:2024/03/29 19:23

我最近接触单元测试比较多,有一些心得,希望能通过写几个专题帮助自己总结一下,这些专题不会花很多篇幅介绍单元测试的基础知识,而是偏重在实际应用中单元测试经常遇到的一些困难。

 

第一篇我想说说对已有代码添加单元测试的解决方案。虽然其中的代码是类似Delphi的,但是并不难懂,应该不会成为其他程序员阅读的障碍。

 

对单元测试有了解的朋友知道,单元测试能够尽早的发现程序中的bug,大大提高代码的第一时间质量,并且可以强制重构(refactoring)代码,辅助项目管理等等。大家一定也知道,单元测试能帮助重构代码的原因是因为单元测试需要代码有非常好的逻辑结构,很多时候,为了测试一段代码,自然而然的就迫使你把代码合理的组织和分离。

 

这时,问题就来了,对于很多已有的项目,逻辑未必清晰,结构未必合理,很多人想为这些项目添加单元测试,怎么办呢?最直接的办法恐怕是重新组织代码,然后进行测试。然而,大家都知道,在绝大多数情况下,这种做法是不太现实的,很少有人愿意动一些不是自己写的、又已经“正常”工作了很长时间的代码,虽然逻辑混乱得像一锅粥,但是只要它还能正常运行,who cares? 而事实上,很多时候,我们为已有代码添加单元测试的目的并不是要完全的测试这些代码的每一个角落,因此,第一时间对代码进行重写对于我们的目的来说可能过于“昂贵”了。

 

打个比方,我们有一个输入单据的窗口,已经被用户使用了3年了,应该说这个窗口的大部分功能还是比较稳定的,但是有一个模块是例外。这个模块主要负责根据用户的输入进行一些自动的计算,比如当用户输入单价和数量后,自动在“总金额”一栏填上前面二者的乘积,随着时间的推移,用户需要更多的自动计算功能,比如向银行贷款,涉及到利息的计算,而如果允许分期付款,也有另外的利息计算,如果允许抵押证券来购买,情况更是复杂多变,但是,由于初期设计的不合理,使得这一部分经常出问题。于是,你决定要给这个模块添加单元测试,每一次添加新的计算规则,都要进行充分的单元测试,解决了大部分用户的要求以后,才交付用户使用。

 

可是,当你分析这个模块代码的时候,发现由于当时设计的原因,很难将其分离出来单独测试。比如,你看到的代码可能是这个样子:

 

Interface

Procedure CalculateMoney();

Implementation

Procedure TForm1.CalculateMoney()

Begin

       UserMgr.Login;

       CurrencyMgr.LoadCurrency;

       HolidayMgr.CheckIfHoliday;

 

       GatherInput;

      

       IffInterest = 0.0 then

              fMoney= fPrice * fNumber

       elseif edtInterest < 0 then

             

       Elseif

       ….

       Elseif

      

      

       End;

End;

 

大家可能看出来了,对于这样一个函数进行测试(我知道这段代码问题很大),如果你直接想在测试代码中运行CalculateMoney的话,你要先完成用户登陆,货币单位的载入以及日期的确认,而且由于需要提取窗口中的输入,你还要生成一个窗口,往里填好数据,这样你才能提取输入,最后才能进行计算,而提取输入之前系统可能还会验证你输入数据的有效性,这样一来,牵扯的方面又更多了。

 

而我们想做的只是测试自动计算部分,我们假设登陆、日期、输入等都没有问题,那么,在不修改原有CalculateMoney的情况下,我们能不能“虚假”的运行诸如登陆、输入这些过程而使得测试转注于计算部分呢?我想,熟悉单元测试的朋友马上就会想到Mocking。没错,Mocking就是我要讲的对于已知代码进行测试的第一个手段。简单的说,Mocking就是利用接口技术,把UserMgr等定义为接口,在测试代码和实际代码中对接口进行不同的实现,达到在测试中简化代码的目的。Mocking的好处是很好的维持了原代码的逻辑结构,而缺点则很明显,就是你需要对原始代码进行较多的改动,比如将UserMgr的类型改成interface,但是,如果这个UserMgr在这个单元里被引用多次呢?比如这里用了一下UserMgr.User,那里用了一下UserMgr.ChangePassword,你就得把所有出现的这些函数、属性统统在接口中定义并且在相应的类中实现,很快的,你就会发现对于一些设计差劲的项目(我觉得,至少80%的项目都属于这一类),这几乎是mission impossible.

 

还好,我们还有一招,对象的继承是我要介绍的第二个手段。对于刚才的代码,以UserMgr.Login为例,你可以先将TForm1修改一下:

 

Interface

Type

       TForm1= class(TObject)

      

       ProcedureLogin; virtual;

 

Implementation

Procedure TForm1.Login;

Begin

       UserMgr.Login;

End;

 

注意到我修改了什么吗?首先,把对UserMgr的调用封装起来成为一个成员函数,然后将这个函数定义为虚拟函数,下一步你可以从TForm1继承一个Form

 

Interface

Type

       TTestForm= class(TForm1)

      

       ProcedureLogin; override;

 

Implementation

Procedure TTestForm.Login;

Begin

       //这里可以用空代码,表示Login总是成功。

       //千万不要用inherited调用TForm1Login哦。

End;

 

到这里,看出来我要干什么了吗?没错,通过继承这种方式,我们可以将一些函数“偷天换日”,在测试代码中,我测试TTestForm而不是TForm1,这样,既维持了原来代码的结构,又可以测试我们感兴趣的部分。那么,是不是说继承就是终极解决方案了呢?不是的,继承这种手段也有它的缺点,就是必须要将想修改的函数定义为至少是protected的,否则,这些函数在子类中是不可见的,另外,这些函数必须是虚拟函数,而虚拟函数会增大虚拟函数表VMT,如果出于测试的原因,刻意将大量函数转为虚拟,那么会影响程序的性能。因此,继承也不是万金油。

 

所以,我的结论有点令人遗憾,目前来说,对于初期设计很差的项目,并没有很好的解决方案可以对其进行单元测试,对于这些项目的局部,可以采取mocking或者继承的方式进行修改。但是,最终极的解决方案只能是重新设计,重新编写。

 

大家还可以参考一下这篇文章,http://weblogs.asp.net/rosherove/articles/DependencyIssues.aspx,我们的观点非常类似,这是因为,就目前的技术而言,做单元测试都会最终遇上这些问题和提出这些解决方案。

最后说一句,如要转载,请和我联系,谢谢。

原创粉丝点击