TDD(Test Driven Development)实践与变化-->TAD(Test Assist Development)

原创时间:2005-02-05 01:56
目前版本:V1.1

前言
  我的实践并非完美的TDD,因为我并没有追求测试的覆盖率,也没有考虑是否会有太多的冗余代码在客户使用的软件中。因为,对于软件的开发的出来的最终产品,我只关心它是否符合了客户的要求,而并不关心实现的具体实现方式。

       TDD,意味着测试驱动开发,意味着一种新开发技巧,它可以带来非常好的质量保证。但在实践中,我并没有完全遵循<<测试驱动开发>>一书里的推荐性实践,这是因为TDD给我的团队带来的一些不是很方便的元素:
  1、测试代码过多,造成开发人员更多的负荷。
  2、对GUI测试过于繁杂,造成大量的时间浪费。
  3、虽然提供了开发过程中真实代码的质量,但这并不意味着测试代码本身会保证自己的质量。
  4、基础模型发生微量改动的时候,往往会造成测试代码变化,从而也造成真实代码的变化,使得代码维护多了一道手续。
  5、外部通过反射调用测试代码时,如果原有代码进行了某些重构后(如重命名类名),外部无法及时反馈变化(不在重构作用范围内),得更新测试代码,虽然面向接口测试可以解决问题,但那不是我的本意。
  6、对于私有函数的测试,往往要通过反射来进行,但反射代码太长,敲打键盘麻烦。
  7、精度控制力度不够,编写可测试代码时,有时需要分割为过多的功能函数,造成部分性能损失。
  8、测试代码覆盖率过高,需要编写一些无意义的测试代码来维护真实代码。

  基于以上因素,也由于一般的项目开发往往时间要求很急迫,所以,我无法完成TDD的所有要素,也无法像书中的方法那样开发,因为此,我的选择只有修剪一下,并更改其中一些方法,使得我开发时更方便一些。

  在我的观念中,与其探索TDD之路,不如研究一下,如何用好NUnit这个工具。NUnit这个工具为我们带来了新的测试手段,尽量采用的技术上,并没有十分称道的地方,但该工具在调试上的强大,是一般工具无法比拟的。
  TAD,算是小发明吧,也可以说是无法完全实现TDD的一种借口,但目前的实践,确实是有效的,也确定提高了一定的编写效率。下面全部是论述TAD实践。

TAD的准备工作 
        修改Visual studio.net的模板文件,可以在*\Microsoft Visual Studio .NET 2003\VC#\VC#Wizards\CSharpAddClassWiz\Templates\2052\中找到该模板(这里是中文版的Visual Stuido.net2003),将它改为以下的代码: 
   

using System; 
#if DEBUG 
using NUnit.Frameworks; 
#endif 
 
namespace [!output SAFE_NAMESPACE_NAME] 

    
/**//// <summary> 
    
/// [!output SAFE_CLASS_NAME] 的摘要说明。 
    
/// </summary> 

    [TestFixture] 
    
public class [!output SAFE_CLASS_NAME] 
    

        
public [!output SAFE_CLASS_NAME]() 
        

            
// 
            
// TODO: 在此处添加构造函数逻辑 
            
// 
        }
 
         
        TDD
TDD 
    }
 
}
 

  这样,每次添加一个类的时候,自动就有了预编译指令,具体的测试代码就可以在#if DEBUG...#endif中进行撰写了。 

       为了避免在实际开发中让实际的项目代码错误地调用测试代码,另一方面也为了避免发布的正式产品中的代码冗余,采用DEGUG预编译是一个不错的实践,如果真实代码中错误地调用了测试代码,在Release时,会很容易发现。同时,在TAD中,更改所有的visual studio 模板,以使其在DEBUG模式下自动支持NUnit的调试方式,是非常有趣的,我很乐意采用这种方式来开发软件,不仅它提供了函数级别的调试功能,让人能够享受开发过程中的乐趣。 

TAD的实践 
       1、对可能的测试进行分类的实践
     代码单元测试在编码过程中的作用是监视代码的输入与输出是否合乎我们需要的结果,这也就意味着,我们需要的对一些复杂代码或不太确信的代码进行测试。相信不少开发人员在开发过程中,得查询一些新的资料,然后利用这些资料中的提供的参考来完成自己实际的功能,有时候,会用到一些不是很熟悉的技术,此时,就应该进行测试代码的编码,测试代码的颗粒应该细到具体的实现细节上。如果是一般性的功能,我们只需要关心大体上的输入输出是否合乎要求,如果不合乎,再进行更细致的测试代码编写,此时,就是TAD了。

  据此,在模型出来后,编写具体代码前,我们先将针对需要测试的功能分拣为以下三类:
  一、UI界面
  二、功能及逻辑
  三、数据访问

  然后,针对不同的类别,初步估计要编写的代码块。
  一般说来,原型制作时,最重要的是一个UI界面的显示,这可以给客户一个实在的观感,在UI原型上设计,最常用与最快捷的方式应该是photoshop、visio之类的绘图软件来实现,而并非实在的软件窗口。
  到了真实的UI界面开发时,测试时,一般不需要编写测试代码,多半的情况下,可以使用机器人来搞定,这可以大大地测试负担,也可以避免因为编写测试代码而耗费太多的时间。

  针对功能与逻辑代码时,应该考虑如下因素:
  1、使用的是熟悉的技术,编写的是简单的功能代码块,如简单的调用数据访问代码,简单的加减乘除等。
  2、使用的是不太熟悉的技术,编写的是简单的功能代码块,如以前从未接触过序列化,现在需要使用该功能。
  3、使用的是熟悉的技术,但代码块比较复杂。比如网络应用程序,因为要保证突发情况下的代码正确,此时的测试就是必要的了。
  4、使用的是不太熟悉的技术,编写的是复杂的功能代码块。这个不用举例,这种一般是技术难点,测试代码肯定是必需的。

  如上,3、4两种情种,测试代码是非写不过,而且代码的撰写要按照编写可测试代码的原则来进行,而对于1、2两种情况,只需要针对大的接口进行测试就行了,比如,测试一个数据访问类中的某个对象是否返回了,不必逐一验证返回的对象是否每个字段都起到了作用或被赋值,因为此时的测试代码编写是不可靠的,因为业务逻辑的变化造成对象属性的增加、删除、更改是非常常见的事情,也是非常头痛的事情,如果此时进行编写的详细的测试代码,带来的维护量是可观的。所以,对于这种代码,一般情况下,应该是测试该操作该对象的代码是有生效,而非详细的对象验证。 

  2、针对数据访问的实践
    最头痛的事情,莫数据访问了,数据访问至今也没有一个十分理想的办法,如果采用真实写入,真实删除来测试数据库中的数据信息的话,是非常吃力不讨好的事情。在数据访问的测试代码上,要注意几个原则,才能减轻负担:
  1、不要编写测试数据库中产生数据的条数是是否合乎需求的代码,如果数据库中有级联更新等操作,你也许会考虑写测试代码来实现,但实际上,你在多加了一条记录后或删除一条记录,产生的测试代码的修改将是惊人的,此时更好的办法是,在数据库中建立一张视图,写上合乎需要的SQL语句来验证,然后在进行指定的操作后,直接在独立于项目之外的地方查看视图(如SQL Server的企业管理器,或Visual Stuido.net中的服务管理器中查看),效率将会高得多,但一定要记得,为该视图取名时,一定要明显区别于其它表或视图,并且在代码中千万不要出现它。
  2、数据库的ID尽量用唯一自动增长字段,或使用uniqueidentifier字段,同时,避免用测试代码增测试ID的值,这样做愚蠢的。
  3、不要指望事务操作可以提供有效的方式来帮助你测试数据,这会造成许多难以控制的问题,并且会增加测试代码的编写长度。
  以上的要点初步看起来是愚蠢的,有些人会认为自己并不会这样做。是的,确实是这样,但在项目组中,总是有人会犯错的,做为项目的掌控者,一定要明白这一点。

  数据访问的良好建议是,如果实在是需要在软件中观看数据访问层产生的结果,建议建立单独的窗体,放上一个数据表格来显示数据结果来专门负责此一操作,当然,这是指这样测试的后果是值得负担的情况下,测试代码中可以调用并显示一个窗体的,切记此一要点,会带来非常便利的用途,而且语句也非常之简单:Application.Run(new 窗口类名()); 

        3、测试代码编写具体实践
        本实践由于是建立在TAD的基础上,TAD的代码编写与普通的测试驱动开写测试代码的位置有一些不同之处。比如实例:

using System;
#if DEBUG
using NUnit.Framework;
#endif

namespace Test
{
    
#if DEBUG
    [TestFixture]
    
#endif
    
public class Sample
    
{
        
public Sample()
        
{
        }


        
private int Sum(int a,int b)
        
{
            
return a+b;
        }

        
        
TDD
    }

}


  上述实例中,SumTestCase(1,4,6)将会激活红条。

  可以发现,如果采用TAD,可以大大地提高测试代码编写的灵活性,主要的表现在于,无须重新构造一个新的对象,测试代码将与真实代码捆绑在一起,同时,拥有了内部私有成员的访问权限,可以对细节代码进行更好的操控。
  当然,一般的情况下,如前面的实践原则,是不需要对上述的Sum函数编写测试代码的,因为它比较简单,你有足够的能力可以操控它。
  上述是一个使用TAD的技巧,是要使用测试驱动,还是要使用测试辅助,可以根据实际的需要进行掌握,有时候,针对一个复杂的程序集,不可避免地需要TDD作为一种引导来开发,尤其是团队里面有严重的水桶效应的时候,但有时候,直接构造测试代码也许比测试驱动的逐步构造来得更快捷,因为你已经非常清楚该测试代码的写法(或者你以前写过,可以直接复用,或进行稍微的调整)。

  TAD中,利用Application.Run()来创建一个窗口的时候,要注意,该函数不应该写在该类内部,因为这是一个外部的调用,建议写在该类的外部比较有效,否则的话,相当于创建了一个实例后,该实例又创建一个新的实例来显示窗口,这在逻辑上多少是令人不爽的。  
  
  对于一般性的TDD测试,一般是对一个类或类中的公共成员的测试,某些测试要求必须在类的外部进行,遇到这种情况时,,也可以在相同的工程项目中进行测试,这里的实践有两种方案:
  A)建立一个类,命名为:XXX_TEST,然后对该类的所有代码都用#if DEBUG....endif来包括,好处是调试时非常方便,这样命名,在解决方案资源管理器中,类的源文件都会被放在一起,所以十分方便,但缺点是不便于集中管理。
  B)建立一个专属的文件夹,命名为如TEST之类的名称,然后所有的对类进行外部测试代码都放在这个文件夹中,好处是方便管理,容易查找,便于集中管理,但缺点是根据一个类查找它的测试代码时,往往要消耗一点时间,在整个项目中,细水长流出来的时间是非常可观的。
  
     4、合理使用region
  在此,非常感谢Visual Stuido.NET中提供的#region...#endregion,设计这个功能的人,太伟大了,让测试代码参杂在程序中时,看起来也是非常的清晰。 

       5、存在的问题及解决思路
  TAD方式的两个不方便的问题:
  1、对于想专门阅读整个项目测试代码的人来说,显得不是很方便。解决方案:做一个专门提取测试代码的软件,重新生成一个项目,里面只包括测试代码。
  2、发布Release时,可以把nunit.Framework.dll的引用删除,但要修改代码时,又得用重新引用,麻烦。做个Visual Stuido.net的Addin,整个可以加入和删除此程序集的功能。

  6、应该注意的问题
  另外,关于在测试的保障下的重构的要注意一点,不要在开发过程中进行重构,这很浪费时间,因为后面的东西是否对你有影响,你并不会很清楚的(记住没有银弹),两顶帽子原则很有用,并且,要注意戴上重构这顶帽子的之前,你戴的另一顶帽子已经告一段落,不要急换帽子,这既不会让人看清你的帽子很漂亮,也不会让你在开发的冬季感到一丝温暖。 

posted @ 2005-02-05 01:56  一根神棍研古今  阅读(2637)  评论(4编辑  收藏  举报
Web Counter