走进单元测试(1):为什么难以广泛应用
这几天在北京参加Qcon,觉得Scrum和敏捷在这里提到的比较多,而单元测试作为敏捷方法的一个重要配套工具,了解和真正进入实施的却并不是很多。
这里并不想告诉你如何做单元测试,这样的文章已经一搜一大把,我没有再写这种的必要。开这个系列,首先是因为我进入了公司的技术创新组,目前主要负责单元测试这块,以期记录自己的成长;二是希望借这个系列,能够为大家带来一些真实的单元测试用例的设计方法和在设计用例中遇到的问题,以及解决问题的方法。
在我6年的技术工作中,如果问我什么技术对我的影响最大,我会毫不犹豫的说是单元测试。我比大多数人幸运的地方就是我的第一份写代码的工作就是编写单元测试;没错,是单元测试,尽管不是TDD的实践。但至少为我理解TDD打下了一个坚实的基础。而6年来,我一直希望能够有一个地方可以真正的将这些东西用起来,现在来看暂时算是等到了希望。
我辗转过很多公司,也曾经提过很多次关于单元测试的问题。然而每每提及的时候会得到支持,但最终实施都不了了之。我一直在思索为什么?为什么一个明明觉得很好的东西大家都嘴上说好而就一直不肯去实践?下面是我认为的几个原因:
1、开发成本的高昂。单元测试会加重开发人员的负担,因为有了更多的代码需要编写。而这种成本的增加在小作坊是一个很重的负担,毕竟对于他们生存是第一要务。即使是在大公司,鉴于很多人没有这样的经验,因此培训和指导如何编写单元测试也是一块很大的成本。
2、时间上的紧迫。从一个软件项目的角度来看,尽管编码所占的时间并不多。但如果是一个小的软件作坊形式的公司,其生存的要点在于能够短平快的做完项目,实施单元测试会延长项目的交付时间。即使是在一个比较规范性的公司,由于客户或者内部需求的时间压力,也很多时候只能选择放弃采用这样的过程,毕竟满足客户需求才是项目的最终目标。
3、开发人员的素质。为什么会有这一条,是因为接触过很多人开发人员,他们仅仅把完成任务当做目标,他们希望的就是尽量以自己熟悉的方式来尽快完成任务,剩下的时间就可以做自己的事了。而这部分人恰恰却是推动过程中的最大阻力。如何说服他们,采取什么措施,后面的系列可能会有这方面的思考。
鉴于中国的软件公司多数还处于小作坊时代以及开发人员的素养还有待提高,我一直比较悲观的认为,就不说敏捷了,就是单元测试的普及,在中国还有很长的路要走。我也希望自己的这个系列,能够为这个事情的推动尽自己的绵薄之力!
走进单元测试(2):必须要自动化
前面讲到了现在被调到公司创新组做单元测试相关的工作,先交代下一些背景:
公司是一个做电子商务网站的公司,规模还可以。目前已经有了一套框架,该框架实现了ORM、同步、缓存等很多的功能;然后整个业务算是构建在这个框架之上的二次开发。我目前的任务是对这个业务代码构建一个相对完整的单元测试集。
既然提到单元测试,就不得不提自动化这一块。这里的自动化包含两层意思:首先,单元测试用例需要一个自动化执行的环境,即一个能够由外部条件触发而自动运行。其次是单元测试用例本身要能够自动化的进行,不能每次运行之前需要先配置好环境之类,也即能满足回归测试的要求。
对于第一个要求,我们使用的是CruiseControl.NET这个持续集成工具来进行自动化的配置。这个工具的具体用法这里不介绍了,具体可参见http://ccnetlive.thoughtworks.com/。注意到我们现在建立测试并不是走的测试驱动开发流程,因此可以采用一个定时的方式来发动集成请求,我们把这个时间设置在半夜,具体做的事情如下:先去SVN上取出最新的源代码和测试代码,然后编译,最后使用NUnit运行生成的测试dll。
下面是一个ccnet配置示例,有兴趣的可以拿来直接去用了:
<cruisecontrol xmlns:cb="urn:ccnet.config.builder">
<!-- This is your CruiseControl.NET Server Configuration file. Add your projects below! -->
<project name="" queuePriority="1" queue="Q1">
<workingDirectory>D:\ccnet</workingDirectory>
<artifactDirectory>D:\ccnet</artifactDirectory>
<category>Unit Test</category>
<sourcecontrol type="multi">
<sourceControls>
<svn> <trunkUrl></trunkUrl>
<workingDirectory>D:\ccnet\source\</workingDirectory>
<executable>C:\Program Files (x86)\Subversion\bin\svn.exe</executable>
<username></username>
<password></password>
<cleanCopy>true</cleanCopy>
<timeout>60000</timeout>
</svn>
<svn>
<trunkUrl></trunkUrl>
<executable>C:\Program Files (x86)\Subversion\bin\svn.exe</executable>
<username></username>
<password></password>
<cleanCopy>true</cleanCopy>
<timeout>60000</timeout>
</svn>
</sourceControls>
</sourcecontrol>
<tasks>
<modificationWriter>
<filename>changelist.xml</filename>
<path>D:\ccnet\changelist</path>
<appendTimeStamp>true</appendTimeStamp>
</modificationWriter>
<msbuild>
<executable>C:\Windows\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
<workingDirectory>D:\ccnet</workingDirectory>
<projectFile></projectFile>
<timeout>600</timeout>
<logger>C:\Program Files (x86)\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MsBuild.dll</logger>
</msbuild>
<nunit>
<path>C:\Program Files (x86)\NUnit 2.5\bin\net-2.0\nunit-console.exe</path>
<assemblies>
<assembly></assembly>
</assemblies>
</nunit>
</tasks>
<triggers>
<scheduleTrigger time="23:30" name="UnitTestSchedule" buildCondition="ForceBuild">
<weekDays>
<weekDay>Monday</weekDay>
<weekDay>Tuesday</weekDay>
<weekDay>Wednesday</weekDay>
<weekDay>Thursday</weekDay>
<weekDay>Friday</weekDay>
</weekDays>
</scheduleTrigger>
</triggers>
<publishers>
<modificationHistory onlyLogWhenChangesFound="true"/>
<xmllogger logDir="D:\ccnet\buildlogs"/>
<email from="" mailhost="" mailport="25" includeDetails="true" mailhostUsername="" mailhostPassword="">
<users>
<user name="Billy" group="buildmasters" address=""/>
</users>
<groups>
<group name="buildmaster" notification="always" />
</groups>
<subjectSettings>
<subject buildResult="StillBroken" value="Build is still broken for ${CCNetProject}, the fix failed." />
<subject buildResult="Broken" value="{CCNetProject} broke at ${CCNetBuildDate} ${CCNetBuildTime } , last checkin(s) by ${CCNetFailureUsers}"/>
<subject buildResult="Exception" value="Serious problem for ${CCNetProject}, it is now in Exception! Check status of network/sourcecontrol"/>
</subjectSettings>
</email>
</publishers>
</project>
</cruisecontrol>
(我这里使用的是MSBuild,也可以使用VS的solution直接编译,详见ccnet帮助)
对于第二个自动化要求,这个本身对被测代码的要求比较高。比如很明显的就是现在都采用了分层架构,但这段代码到底属于哪个层,很多人却并不是很清楚。举个例子:业务逻辑层的代码中出现了HttpContext这个东西,那么对单元测试的编写就是个很大的烦恼;再比如cookie等的设置。至于原因,从第一条可以看到要保证这个测试能自动化,那么其运行环境不能依赖于IIS或者WebServer,因此这也是单元测试设计工作中最难的地方,后面将会有很大的篇幅讲述如何消除掉这些依赖或者模拟出一个环境来。
可能有人会问为什么要自动化,同样从上面的两个方面来说。第一个方面的自动化,用Email通知下相关人员可以很快知道测试中有些什么问题,需要他们的协助。第二个方面的自动化则是出于第一个方面的需求,因为如果每次测试都要人工干预的话会很麻烦,成本很高。
走进单元测试(3):消灭HttpContext的依赖,兼谈单元测试的设计辅助性
前篇提到过由于我们已经有了一个现成的平台,现在要对其进行单元测试的补完。而在这个过程中,就出现了HttpContext这类东西,其依附于一个host环境,对单元测试的自动化是一个很大的阻碍。
对于HttpContext,如果没有一个web托管环境,其中的Request和Response等只读属性根本就无法造出来。而如果要搭建一个 web托管环境,不仅为测试带来了干扰(因为要确定是否是托管环境的问题),而且给测试的自动化带来了不方便。那么怎么去解决这个问题呢?
在MSDN中我们可以查到一个叫SimpleWorkerRequest的东西,这个东西的提供了一个简单的 System.Web.HttpWorkerRequest的实现,使得我们可以在IIS之外托管ASP.NET应用程序。而当我们使用 reflector来查看这个东西的源码的时候,发现其中的一些方法很有趣:
[ComVisible(false), AspNetHostingPermission (SecurityAction.InheritanceDemand, Level=AspNetHostingPermissionLevel.Minimal), AspNetHostingPermission(SecurityAction.LinkDemand, Level=AspNetHostingPermissionLevel.Minimal)]
public class SimpleWorkerRequest : HttpWorkerRequest
{
//...
public override string GetHttpVerbName()
{
return "GET";
}
public override string GetHttpVersion()
{
return "HTTP/1.0";
}
public override string GetLocalAddress()
{
return "127.0.0.1";
}
public override int GetLocalPort()
{
return 80;
}
//...
}
这果然是一个简单的实现啊,把IP地址,Http版本,端口等全部硬编码了。
|