TDD的魅力

    06年,我学习Ruby on Rails的时候,首次听说TDD。那时我刚刚进入软件世界,虽然很多概念都是模模糊糊甚至完全不懂,然而还是感受到Rails的简洁,优雅,高效,简单,相较于ASP.NET。

    现在,一直跟随.NET阵营。虽然,在软件世界,.NET的名声并不怎么好,但是经过自己的学习和实践经历:技术和框架只是一个实现的方式和工具,它不足于评判任何关于人的因素,比如认为.NET的程序员就不如Java。软件设计重要的是软件方法学,设计原则,思想,这才是衡量一个程序员的应该考量的因素。如果,站在技术和框架的角度认为什么就比什么好,那这个人一定也是没有多大水平的。我记得以前一篇文章将程序员分类,最高境界就是与框架无关。

    然而,有一个现实却是不利于.NET阵营。似乎.NET程序员不太关注敏捷实践,不太关注设计模式,设计原则。他们通常把所有的代码逻辑放在一个叫做“后台代码”的文件中,他们通常不太注重分离,不太了解依赖注入,他们被ASP.NET的生命周期和服务器控件的回调搞得晕头转向,却最后不懂Web的基本原理;他们花大量的时间在新工具的使用却很少了解技术后面的本质。比如,学习Rails,你起码能了解到REST的概念,了解MVC因此关注分离,了解MVC自然的也会关注TDD,在Java社区,还有大量狂热的程序员交流和分享敏捷实践,设计原则等等,有这样的环境,你可以学习很多很多优秀的软件设计思想。

    所以这些因素,导致.NET程序员成长很慢,常常是.NET程序员对自己信心不足,他们要花费更多的时间,才有机会去接触这些思想。

 

    然而,尽管如此,我仍然热爱.NET。在我眼里,.NET,Jave,Ruby以及其他任何语言和框架工具都是一样的,技术的原理是相通的,如果ASP.NET的教程没有讲解太多思想,看Java,PHP之类的书籍会给你解惑不少,比如对于Web设计基本知识,我觉得任何开发人员都应该看一下《RESTful WebService》,虽然它是用Rails写的例子。比如,《构建高性能网站》虽然是PHP语言,但是你不看实在可惜。如果,你将自己局限在ASP.NET的世界,那是自己对自己不负责。

    我很希望,咱们.NET程序员,也能充分接触和学习更多的软件思想,更扎实的提升自己的技能,相信那是一个你会惊讶的世界!

    我们从TDD开始。

 

    在学习Rails的时候有一件事印象特别深刻,就是几乎Rails的招聘都要求掌握TDD开发。那时候,对于Rails我其实模模糊糊的概念,那对于没有多少软件实践经验的人来说,了解TDD也根本是不可能的事情。

    一直拖到现在,4年多了!

    还记得,在08年的时候,在一家公司兼职,发现可以用VS2005断点调试。那一次可真是激动了一番,因为之前我做ASP.NET都是每次运行页面查看效果。很自然,从那自后我进步了不少,因为VS调试可以做更多的事情。

    今年初,在公司。公司的技术顾问台湾Ruddy大师提出项目开发要采用TDD,后来还做了一次培训。因为我多年来有个TDD梦(就是你知道那是个好东西,但是一直不知道它到底是个什么东西),所以,我认认真真的听完培训,2个小时过去了,我仿佛明白:TDD就是在写每个方法的时候建一个测试方法,等方法写完之后,运行测试方法来检查运行结果。

    这个有屁用啊,后来我仔细一想。我的方法本来就是要返回这个结果,这个测试简直就是多次一举。虽然心里在想大师既然这样要求,肯定有其中不可告人的秘密,但是我对TDD没有抱有太大的信心了。此时,公司还是有同事在按照程序给每个方法添加测试类,但是我猜他可能也不完全懂得这是个神马东西。

 

    直到最近买了两本书《ASP.NET MVC 实践》和《敏捷软件开发-原则,模式与实践》。

    在买书之前,我看到这两本书的书评都很好;但买书之后,才发现,这两本书上都有个共同的东西--没错就是TDD。并且作者以非常肯定的语气告诉我:”我的例子大都与TDD有关,如果这次你还不打算好好学习TDD,那么我也没有打算要让你从我这里学到什么知识“

    无赖之下,慌忙寻找TDD的资料,认认真真去了解TDD,这一次下决心去解开4年多的心结。

 

    经过几天的了解,我发现原来TDD真是个很好的东西,就像我一直以为的那样。其实有一个重要的因素,如果我还是先看TDD教程:先见测试类,然后测试方法。这样可能我又一次会迷失。这次对亏《ASP.NET MVC实战》这本书,作者对TDD的思想做了简单的分析,使我一开始就有了不同的看法。建议大家看看这本书,很好的书。

    为了告诉你我的激动的体验,我们先来看一下TDD实践的步骤,虽然这也可能使你会有和我一样的感觉,但是稍后我就会分析消除你这种感觉。

    像很多书推荐的,我采用NUnit,当然你可以用MSTest或者其他,都是一样的。首先需要下载安装2个东西:

    NUnit:   http://www.nunit.org/index.php?p=download

    TestDrived.NET:   http://www.testdriven.net/download.aspx  (这个需要收费,选择Personal Version即可)

 

    TDD的使用方法很简单,大体思路就是引用一个程序集,然后对程序集中某个类的某个方法进行调用,在调用方法的时候我们可以构造方法需要的参数,我们可以声明该方法执行之后应该返回什么值,或者产生了什么影响,下面我们看一个方法,该方法是在用户用OpenID登录网站的时候,网站首先检查该OpenID账号是否已经注册过,如果没有,则需要OpenID服务器返回用户的基本信息以初始化账户,如果已经注册,则只需要OpenID服务器授权即可:

      public static UserInfo GetUserByOpenID(string openid)
        {
            if (string.IsNullOrEmpty(openid)) {
                throw new ArgumentNullException("openid");
            }

            return "根据OpenID条件查询数据库,返回用户信息";
        }

    现在我们要测试这个方法,我们建立一个ClassLibrary的项目,添加上述方法所在的程序集引用,然后建立测试方法,在建立测试方法之前,我们要想一下,我们到底要测试什么。我们测试是希望保证这个方法不会出错,因此我们在测试的时候,就会思考,哪些因素可能会导致这个方法出错,并且预期会返回什么值,对于上述方法,我想到的有三种情况:

    第一,参数为空

    第二,如果数据库中包含某个OpenId号,则返回值不为空

    第三,如果数据库中不包含这个OpenID号,则返回null

    所以,我们要测试这三种情况是否会如期进行,我们编写测试类:

    [Test, ExpectedException(typeof(ArgumentNullException))]
    public void GetUserByOpenIDTest()
    {
        UserInfo user = UserHelper.GetUserByOpenID("hielvis.myopenid.com");
        Assert.IsNotNull(user);   ///数据库中包含这个用户,返回值不为空

 

        UserInfo invalidUser = UserHelper.GetUserByOpenID("jack.myopenid.com");
        Assert.IsNull(invalidUser);    ///数据库中不包含这个用户,应该返回null

 

        UserHelper.GetUserByOpenID(null);   ///参数为null,应该抛出ArgumentNullException异常
     }

    编写好测试类之后,我们就可以运用TestDriven.NET来运行测试了,在我们的测试方法GetUserByOpenIDTest()上右键就可以运行测试,我们可以直接运行Run Test(s)只执行测试,我们还可以选择Debugger进行调试,可以看到哪里出错,以及我们预期的是否正确:

 

    我们也可以用NUnit自身的GUI工具来运行测试,它可以打开一个VS项目,并自动检测包含[Test] Attribute的方法:

 

    你的第一反应是:这么简单?也许比你遇到的大多数技术都要简单,比如我们前面讨论的Silverlight布局,控件面板自定义等等。

    你的第二反应是:这有什么用,我的方法本来就是这样运行的,不是多此一举!

    下面列出一些对TDD认识的误区:

误区一:没什么用处,多此一举

    看了上面的例子,我们很容易想到这没什么意思,没什么用处,这也是4年来我主要的想法,也是阻止我去学习它的原因。

    有人会说,我在编写方法的时候本来就是考虑了这些因素的,并且我在调用的时候也加了判断条件。我以前从来不写单元测试,系统“一直按我期望的那样正常运行”。

    问题是,你相信你写的代码吗,你敢保证每一个方法,你都这样去思考过吗:它应该返回某个期望的值,如果参数是一些边界值,它应该返回这样而不是让系统崩溃。也许大多数时候,你都是匆匆想一下,马上就写方法名,方法体,你也许考虑了主要的因素,但是是否这个方法能处理你没有预期到的条件。

    你是否有这样的感觉,越是软件做到后面,你越不敢保证软件不会出Bug;当别人在一边竖起大拇指称赞系统多么稳定的时候,你的心总是悬空的,你知道它随时都有可能出现问题。

    TDD要求在每个方法定义编写前,去考虑方法的各种可能情况,并且直到测试通过,才开始编写下一个方法。它是你在编写最小单元功能的时候,确保每一个功能单元是更加健壮的,因此称作单元测试。

    TDD的神奇力量不在于那段测试代码,那只不过是一个普通方法的调用,验证而已。

    TDD最宝贵的是:促使你在设计每个最小功能的时候,花一点时间去仔细思考这个最小单元(方法)的各种边界条件,确保每一个单元更加健壮,稳定。这样,到最后,你的整个系统也更加可靠文档。

    只有经过测试的代码才是可靠的。虽然Bug不可避免,但是,如果你做了严格的单元测试,你会对你的代码有更多的信心。

 

 误区二:浪费时间

    TDD要求在每个方法定义编写之前,先写测试代码,即你要花一点时间去思考这个方法的各种边界条件,调用时会出现的各种情况。

    这对于我们平时总是拿到一个功能,就开始定义类写方法相比较,却是是会花点时间。但是如果最终比较,它并不浪费时间:

    你是否有这样的感觉,到一个比较大的功能快完成的时候,你会花很多时间去调试。到后面,每一个Bug的调试,都会花费相当大的时间去定位和排错。常常,我们在一大堆断点之间跳来跳去,只是因为某个引用为null。并且断点调试并不是那么顺利的,有时候你需要运行几次才能够定位到bug的地方。幸运的是,也许你凭经验知道大概的位置,这可以范围,但是不可避免的是,你需要花费更多的时间。

    而经过单元测试,每一个方法都经过了足够仔细的考虑,这将大大减少后期Bug的频率。原因很简单,你在设计一小块功能的时候,也许考虑得比较仔细,但是当一个单元被整合进一个大的系统,在复杂的系统环境下,你没有考虑到的因素就暴露出来了。并且系统越到后面,问题越多。

    自己好好算算,这样的时间你浪费了多少。

 

    最近的项目,一直在尝试TDD实践,虽然不是很顺手,还是有些感悟,总结给大家:

    TDD的好处:

好处一促进代码规范,设计结构合理,更遵循好的设计原则

    刚开始接触单元测试是会遇到挫折的,因为你会发现你编写的方法难以测试。比如参数太依赖另一个方法或者对象,参数不可构造,方法太复杂,功能混乱导致边界条件太多,等等,这些都是不良的设计。

    遵循好的设计原则,比如单一职责,方法有清晰单一的任务,比如依赖于接口而不是实现的参数,不仅有助于减小耦合,在测试的时候更容易构造接口实现的参数等等。因此单元测试反过来促进你遵循更好的设计思想。这里引用《ASP.NET MVC实战》里面一句话:“我们极度关注控制器类的测试是因为测试驱动控制器确保它们有良好的设计。对拙劣的代码进行测试驱动几乎是不可能的”。

 

好处二:精准的定位错误的地方

    因为测试的是最小的功能单元,能最小时间代价的获取错误位置和原因。

 

好处三:减少调试时间

    前面我们分析了,在系统后期调试会花费的时间代价。

    其实,不光从系统健壮,可靠等角度考虑单元测试。从功能设计调试的角度,我们更应该时常利用单元测试的好处。

    前面的例子,需要检查OpenID账户是会存在,如果不用单元测试或者类似的思想,我们需要运行网站,并以一个示例的OpenID账号登录,然后获取用户输入的OpenID,接下来才是我们真正要验证的地方,看看,为了测试这个小小的方法,我们要花费多少时间,而如果我们用前面的单元测试,这时间是不是大大缩短。

    再举一个例子,我现在的项目会实现一个IRestFactory,IRestFactory查找返回一个IRestHandler:

    public interface IRestHandler : IHttpHandler {

          bool IsMatch(HttpRequest request, string requestType);

    }

    IRestHandler的实现类定义了路由规则,不同的请求地址会有不同的IRestHandler来处理,我一共有43个处理接口,每个接口API还包括验证机制,因此不能用浏览器直接输入地址来模拟,必须用程序运行才能检查。

    现在我需要首先验证这43和API是否能够被正确路由,如果按照以前的思路,我得运行程序,经过复杂的页面登陆,调整等等之后才能测试一个API,这样我可能一天读弄不完这些事情。但是借助单元测试的思路:

    我要测试什么,就是要测试以下方法对不对:

    IRestFactory.Gethandler(HttpContext context, string requestType, string url, string pathTranslates)

    这不就是跟单元测试类似了吗--测试一个方法。但是我发现HttpContext是不可构造的,怎么办,仔细一检查,原来IRestHandler.IsMatch(HttpRequest request,string requestType)方法并不需要HttpContext,而HttpRequest是可用以构造的,因此要测试路由功能,我只要另外添加一个方法:

    GetHandler(HttpRequest request, string requestType,string url, pathTranslated),只要这个方法是通过的,那么路由就没有问题,于是剩下的事情就是针对这个方法进行单元测试,不同的地址只是一个字符串参数,根本不需要运行整个应用程序,我只要写43个断言即可,这不是简单多了吗?

    所以,恰当的运用单元测试,可以让你的程序调试更得心应手。



好处四:更健壮,可靠的代码,可以睡好觉

    发现,开始TDD之后,我对代码更加有信心,不会时时担心这会出问题你也会出问题,虽然Bug难免,但是经过测试的代码更加可靠,这样是不是能多睡觉,少加班呢,更重要的是减少不少焦虑细胞。

 

    总结:本篇并非TDD教程,而是自己入门心得,更详细的使用还需要参考专门的教程。推荐:

    《asp.net mvc 实战》

    《敏捷软件设计-原则,模式与实践》

    《单元测试之道C#版-使用NUnit》

     更多的需要实践体会,欢迎讨论!

 

posted on 2010-12-31 14:33  秦春林  阅读(3236)  评论(24编辑  收藏  举报

导航