编写高质量的代码---单元测试 Nunit+NCover
一、 前言
测试,对于软件人员来说应该不会陌生,尤其是测试人员,因为在每个项目或产品在由开发人员作完编码后都会有相关的测试环节。当然小公司也许测试环节比较弱,而大公司则对测试非常重视,微软的开发团队中测试人员和开发人员的比例达到了3:1,足见测试作为保证软件正确性检查的关键环节是多么的重要。当然,对于这些功能测试,集成测试等都是面向测试人员的,开发人员只需要将编写好的程序交由测试人员,然后自己根据测试结果进行Debug。我们有没有办法在开发人员这一级就实现很好的质量保证呢,这样在我们提交到测试人员的程序就会包含尽量少的BUG,答案就是使用单元测试。当然,现在的单元测试工具也很多,今天我要讲的是一款在.net下比较优秀的开源的测试工具——NUnit 及其辅助工具 NCover。下载地址是:http://www.NUnit.com http://ncover.org/site/
二、 单元测试的必要性
很多编程人员都遇到这样一个问题,就是当自己编写完代码,有经验的朋友也许会凭自己的经验和理解也反复手工测试了一通,然后拍拍胸脯说:“OK,没问题!”,然后交由测试人员。但经过测试人员一折腾,可能马上就揪出了一大堆Bug。然后,开发人员就疑惑了,为什么我还有这么多问题没有考虑到呢?当然,也许经验老到的开发人员出的问题会少一些,但是,对于缺乏经验的开发人员难道就没有办法保证提交高质量的代码吗?其实不是,通过自动化的测试工具完全可以弥补这些不足。
现在有一种说法叫测试驱动(Test-Driven)的开发方式,认真做好单元测试的同时,其实也是在迫使你优化自己的代码结构和逻辑。
当然,单元测试的好处还有很多,他可以让你对自己的代码更加自信,使用它,你会喜欢上它的,让我们现在就开始吧!
三、 单元测试的相关技术
本文章我们主要是讲解如何通过NUnit测试代码逻辑和单元测试伴侣NCover查看代码的运行覆盖率,从而保证完整地进行测试。
四、 灵活的单元测试框架---NUnit
在讲解实际的例子之前我们还是先来学习一下单元测试的必备知识吧。
(一)NUnit的安装
安装NUnit后,我们会在安装目录下发现一个Doc目录(存放NUuit的帮助文件),一个Src目录(存放当前版本NUnit的源文件,有兴趣的朋友可以作为学习参考),还有一个是bin目录,bin目录中存放了NUnit的控制台程序和GUI程序,我们可以通过两种方式来使用NUnit,NUnit的控制台程序主要用于自动化的测试,协同自动化的编译工具一同使用,如NAnt。而NUnit的GUI程序则通过可视化的界面提供给用户测试。还可以安装集成到VS.Net的NUnit测试插件(下载地址:http://mutsntdesign.co.uk/nunit-addin/),方便在开发环境中进行测试工作。本文主要介绍前两种方式。笔者当前的Nunit版本2.2
(二)NUnit的语法
进行NUnit单元测试时需要编写相应的测试代码,代码也是使用标准的C#语法,并可以使用NUnit框架提供的一系列基础的功能。在编写测试代码前请保证你正确的安装了NUnit,并在项目中引用了NUnit.Framework,并在代码文件中引用了NUnit.Framework命名空间。
下面是开始前的几条建议:
l 建议对每个重要的、复杂的、易出错的、包含重要业务逻辑的方法进行单元测试,测试代码需要有权访问到产品代码(被测代码);
l 测试代码可以和产品在同一个项目中也可以在不同的项目中,视具体情况而定;
正常的测试流程:
l 准备测试所需要的各种条件(创建所需要的对象,分配必要的资源等)
l 调用要测试的方法
l 验证被测试方法的行为和期望是否一致
l 完成后清理各种资源
先来看一个简单的产品代码和测试代码
产品代码:
using System;
namespace NUnitDemo
{
///<summary>
/// Class1 的摘要说明。
///</summary>
public class Class1
{
public Class1()
{
}
//演示处理结果的正确性和异常的处理
public int GetMaxItem(int[] ints)
{
int Max = ints[0];
foreach(int intItem in ints)
{
if(intItem > Max)
{
Max = intItem;
}
}
return Max;
}
}
}
测试代码:
using System;
using NUnit.Framework;
namespace NUnitDemo
{
class TargetClass
{}
///<summary>
/// Class1_Test 的摘要说明。
///</summary>
[TestFixture]
public class Class1_Test
{
public Class1_Test()
{
}
[Category("simple")]
[Test]
public void TestGetMax()
{
Class1 Cls1 = new TargetClass();
int[] Ints = new int[10]{1,2,3,4,5,6,7,8,9,10};
Assert.AreEqual(10,Cls1.GetMaxItem(Ints));
Ints = new int[10]{1,2,3,4,5,6,15,8,9,10};
Assert.AreEqual(15,Cls1.GetMaxItem(Ints));
}
}
}
代码中,上面是一个实现了获取整数树组中最大值的完整的产品代码程序,下面是该产品代码的测试程序,我们可以看到测试代码也是标准的C#代码,不过使用了一些NUnit 框架特有的方法和属性。
测试中断言测试的主要组成,其中大多数都是包含在Assert类中的静态方法。下面我们来一一介绍。
l AreEquals
语法:Assert.AreEqual(expected,actual[,string Message])
描述:这种断言应该是使用最多的一种,expected是期望得到的值,actual是被测代码产生的值,Message是可选的,如果提供该参数,则在错误发生时,会报告该消息。
l IsNull
语法:Assert.IsNull(object [,string message])
描述:验证一个对象是否为null,对象不是null则验证失败, message可选。
l AreSame
语法:Assert.AreSame(excepted ,actual[,string message])
描述:验证excepted和actual是否引用的是同一个,message可选。
l IsTrue
语法:Assert.IsTrue(bool condition [,string message])
描述:验证一个二元条件是否为真,message可选。
l IsFail
语法:Assert.IsFail([string message])
描述:该断言回使测试立即失败,message可选,常用于验证某个不应运行到达的地方。
l 自定义断言
虽然NUnit提供的大部分断言都已经能够满足日常测试需要,但如果遇到了特殊的测试要求就需要自己自定义测试断言。自定义断言由自己实现一个公共的测试方法的逻辑,然后在测试时调用。
测试中也常通过属性的方式来标记代码块的作用,下面介绍常用属性。
l [TestFixture]
用于标记一个测试类,该测试类可以包含多个测试方法,
l [Suit]
用于将多个TestFixture组织在一起,任何一个测试类都可以包含一个用[Suit]标记的静态方法,该方法返回一个TestSuit,该TestSuit是TestFixture的集合(注意:使用TestSuit类时请添加NUnit.Core;的引用)
l [Test]
标记一个测试方法
l [Category("name")]
l 用于把不同的测试方法进行分类管理,使用相同[Category("name")]标记的Test可以一起运行,弥补了[Suit]只能对TestFixture分类的不足
l [SetUp]
标记用于在每个测试方法开始前需要执行的方法,如申请某些资源
l [TearDown]
标记用于在每个测试方法结束后需要执行的方法,如释放某些资源
l [TestFixtureSetUp]
标记用于在每个测试类开始前需要执行的方法,如申请某些资源
l [TestFixtureTearDown]
标记用于在每个测试类开始前需要执行的方法,如申请某些资源
五、 使用NCover协同工作
介绍了那么多NUnit方面的知识,现在来了解一下如何使用NCover,看看它如何来为NUnit增色。
我们都知道在测试一个程序的时候,最苦恼的莫过于自己不能想到所有的可能情况,往往会漏掉很多的重要的Case,致使前期无法发现的错误遗留到后面,为后面的编程和测试工作带来很大的负面效应。当然这些情况我们是可以尽量避免的,利用自动化的代码覆盖工具更能在这方面游刃有余。
安装完NCover后可以在安装目录下发现
NCover是一个命令行工具,用法为:
Usage: NCover /c <command line> [/a <assembly list>]
/c运行需要检测的应用的命令行.
/a需要检测的程序集的列表. 如 "MyAssembly1;MyAssembly2"
/v允许详细的日志信息,该命令行非常适合于测试,不过会使得coverage变的非常大
NCover会在检测完代码运行后生成3个文件
· Coverage.log – 该文件主要是记录覆盖检测期间产生的事件和消息日志,还有错误日志,当然,如果使用verbose logging,则会包括更详细的中间语言相关的信息
· Coverage.xml – NCover的分析输出文件. 例子如下
· Coverage.xsl - Coverage.xml的转换文件,便于将Coverage.xml转换成友好的表 格形式显示
转换后的Coverage文件
Visit Count | Line | Column | End Line | End Column | Document |
1 | 48 | 13 | 48 | 58 | C:"Dev"Utilities"ncover"NCoverTest"NCoverTest.cs |
1 | 49 | 13 | 49 | 22 | C:"Dev"Utilities"ncover"NCoverTest"NCoverTest.cs |
1 | 50 | 17 | 50 | 24 | C:"Dev"Utilities"ncover"NCoverTest"NCoverTest.cs |
0 | 51 | 13 | 51 | 48 | C:"Dev"Utilities"ncover"NCoverTest"NCoverTest.cs |
0 | 52 | 9 | 52 | 10 | C:"Dev"Utilities"ncover"NCoverTest"NCoverTest.cs |
如上所示,我们可以很方便的得知哪些文件,哪些行,那些列的代码被访问了多少次,这样就能很有针对性的对当前产品代码编写测试代码,测试是每个代码路径都能测试到位。
(题外话:笔者认为如果能和VS。Net的IDE集成的话,从而能在代码中很直观的看出哪些代码执行了多少次,哪些没有执行,并以不同的颜色来区分,则会更友好。呵呵,不过这些大家不用担心了,盖茨和他的那帮兄弟们都早就为我们想到了,在VS2005里面就集成了单元测试和代码覆盖工具,非常的强大。不过今天是讨论NCover,还是多讲点NCover的优点吧!)
如我们要测试D:"DemoAppFolder"NUnitDemo"TestPrj"bin"Debug下的TestPrj.exe
则可以使用如下命令(请确认在环境变量中添加了NCover的安装目录):
此时
表示成功,并讲结果存在了Coverage.xml中,可以通过IE等工具进行查看,在Coverage.txt中保存了更详细的日志信息。可以将这些信息作为编写测试方法的依据。
由于NUnit是通过反射的方式对测试方法进行调用,所以说对于测试方法是由NUnit框架驱动运行的,所以在使用NCover中进行代码覆盖测试的时候应该输入NUnit命令行,但是NCover的/c中不支持该命令行,如:NUnit-console testprj.exe,所以替代的解决方案是把复杂的命令行写在批处理文件中,这样将该.bat文件做为NCover的/c中的参数就OK。
六、 单元测试高级主题
(一)如何组织测试代码
如何有效地组织单元测试代码,这个一直是开发人员比较关心的问题。
在小型项目中,也许你并不在意测试代码放在哪,也许放在产品代码的工程中会更省事,只要能区分就行;在大型项目中就不能这样了,应该很好地组织自己的代码,最好建议建立在单独的工程中,作统一管理。但无论是使用哪种方式,有一点是必须的,就是你的测试代码必需能访问到被测的产品代码。如果在同一个工程中的测试代码可以测试protected和internal修饰的方法;但如果是外部程序集则需要要么编写一个继承产品类的新类作为测试目标,要么就干脆将要测试的代码Open开来,申明为public,测试完后在改过来,但这种方式也许会在改的同时引入错误;再者,可以专门写个公用方法来调用需要测试私有方法,达到测试目的,等等,不管怎样,需要尽量保持测试的结构清晰,有条理。
同时,就个人的感受吧,还是给点建议。
l 不能盲目的频繁测试,这样会影响正常的编码,同时也削弱了测试的意义;
l 最好在重新编译程序的时候,自动运行编译成功后的测试脚本,既省事,又能及时发现不足;
l 当然能把单元测试和一些自动化构建工具,如NAnt协同工作能达到更好的效果,在下面主题中,我将介绍这方面的内容
(二)真实环境的替身----MOCK
MOCK---仿冒品,仿制品,顾名思义就是替身,在NUnit里面也是需要替身的。
有没有遇到过这样的情况,当你在编写完一段代码的时候,你需要对这段代码进行测试,但糟糕的是要运行这段代码需要很多的外部支持,如需要日志组件的支持,需要数据库连接的支持等,我真的需要具有所有这些资源才能正常测试吗?答案是否定的,这个时候我们就可以使用替身来模拟这些外部的测试环境,当然这些MOCK我们可以自己写,也可以使用现在网上比较多的免费的MOCK框架,提供了非常多的MOCK对象供使用。其中一款比较优秀的是DotNetMock,可以从http://www.mockobjects.org进行下载。框架中包含了许多日常开发中单元测试会用到的MOCK,同时还提供了动态MOCK(下面会进行讲解)。
那么,在什么情况下需要使用MOCK对象呢?
l 真实对象的行为不可确定(也就是说真实对象没有一个可以预见的行为)。
l 真实对象很难被创建(当我门在进行远程访问方面的开发的时候,不一定时时都有创建远程对象的条件)
l 真实对象很难触发(如网络错误)
l 真实对象令程序的运行速度很慢(如果某个对象的执行会耗用很多的硬件资源的话,将会这一点)
l 真实对象是一个UI或着包含一个UI
l 测试需要询问真实对象他是如何被调用的(例如:测试可能需要验证某个回调函数是否被调用了)
l 真实对象并不存在(在协作开发中会出现这种情况,如果你需要测试的一个方法依赖的真实对象别人还没有完成,这时就会使用接口实现一个MOCK来测试)