在.NET开发中的单元测试工具之(1)——NUnit
nunit是一个专门针对于.net来写的单元测试框架,它是xunit体系中的一员,在xunit体系中还有针对java的junit和针对c++的cppunit,在开始的时候nunit和xunit体系中的大多数的做法一样,仅仅是将smalltalk或者java版本转换而来,但是在.net2.0之后它加入了一些特有的做法。nunit的官方网站是:http://www.nunit.org/,目前的最新版本是:2.6.2。
nunit下载与安装
nunit的每个版本都提供了两种形式的下载:安装文件和免安装方式,分别是*.msi格式和*.zip格式。前者需要安装才能使用,并且会在安装过程中创建一些快捷方式和注册nunit的dll到gac,这样以后编写nunit测试类的时候添加nunit的dll就像添加.net framework的dll一样。如果是下载的zip格式的文件,则不会创建快捷方式和注册dll,在编写单元测试类时需要手动指定nunit的dll的路径。
nunit的运行有三种方式:命令行和图形用户界面。以周公当前电脑上安装的nunit2.5.10为例,安装路径为:c:\program files (x86)\nunit 2.5.10,其下有三个目录:bin、doc和samples。在doc目录下是软件的文档(英文),在samples目录下则是一些样例代码。如果是采用免安装模式的话,运行nunit就需要运行bin目录下的文件,在bin目录下有net-1.1和net-2.0两个文件夹,分别对应.net的不同版本。
下面介绍如何以不同的方式启动nunit:
命令行模式:运行nunit-console.exe。
图形用户界面模式:运行nunit.exe。
并行(parallel)模式:运行pnunit-launcher.exe。
注意:.net2.0版本的nunit是使用/platform:anycpu参数来编译的,我们知道这样的结果是运行在x86的上会被jit编译成32位的程序,而在x64的系统上会被jit编译成64位的程序。如果使用nunit在x64系统上测试32位的程序就会带来问题。为了避免这个问题,可以使用nunit-agent-x86.exe/nunit-x86.exe来测试,因为在编译的时候使用了/platform:x86作为编译参数。
下图是运行nunit的gui界面:
nunit的常用attribute标记
这些都是可以用来作为类或者方法的属性,它们都是system.attribute类的直接或间接子类,有如下:
category:用来将测试分类。这个在主界面可以看到tests/categories两个选项卡,如果给方法标记了category属性就会在categories选项卡中看得到。
combinatorial:用来将来测试时需要测试各种可能的组合,比如如下代码:
[test, combinatorial]
public void mytest(
[values(1, 2, 3)] int x,
[values("a", "b")] string s)
{
string value = x + s;
assert.greater(2, value.length);
}
测试时实际会测试6种情况:mytest(1, "a")/mytest(1, "b")/mytest(2, "a")/mytest(2, "b")/mytest(3, "a")/mytest(3, "b")。
culture:设置测试时的语言环境,这对我们测试一些语言敏感的场景下有用,比如datetime.tostring()在不同语言环境下得到的字符串并不相同。
description:用于指定测试对应的描述,如果选择将测试结果生成xml文件,那么就会在xml文件中看到这些描述。
expectedexception:指出执行测试时将会抛出exception。
explicit:如果测试的类或者方法使用此attribute,那么在使用带gui的nunit测试时这个类或者方法必须在界面上选定才会被执行。
explicit:忽略某个测试类或者方法。
maxtime:测试方法最大执行的毫秒数,如果程序的执行时间超过指定数值,那么就会被认为测试失败。
random:用于指定如何随机生成参数来测试方法。如下面的代码:
[test]
public void testdemo1(
[values(1, 2, 3)] int x,
[random(-10,10,2)] int y)
{
assert.greater(x + y, 0);
}
表示方法testdemo1会生成6个测试,1,2,3分别作为参数x的值与两次从-10到10之间的随机数y组成6次测试。
range:指定参数的方法,如下面的方法:
[test]
public void testdemo2(
[range(0, 11, 4)] int x)
{
assert.areequal(x%3,0);
}
表示从0开始递增,步长为4,且不大于11。
repeat:将重复测试的次数。
requiresmta:表示测试时需要多线程单元(multi-threaded apartment)。
requiressta:表示测试时需要单线程单元(single-threaded apartment)。
setup:在每个测试方法开始之前执行的初始化操作。在nunit 2.5之前要求每个类只能有一个带setup属性的实例方法,但在nunit 2.5之后则没有次数和必须是实例方法的限制。
teardown:与setup的作用相反,是在每个测试方法执行结束之后执行的方法。在nunit 2.5之前要求每个类只能有一个带setup属性的实例方法,但在nunit 2.5之后则没有次数和必须是实例方法的限制。
test:用来标记需要测试的方法,在nunit 2.5之前只能用于标记实例方法,在nunit 2.5之后则可以用于标记静态方法。
testcase:标记方法具有参数并且提供了在测试时需要的参数。如下面的代码:
[testcase(12, 3, 4)]
[testcase(12, 2, 6)]
[testcase(12, 4, 3)]
public void dividetest(int n, int d, int q)
{
assert.areequal(q, n / d);
}
将会执行三次测试,相当于:
[test]
public void dividetest()
{
assert.areequal(4,12/3);
}
[test]
public void dividetest()
{
assert.areequal(6,12/2);
}
[test]
public void dividetest()
{
assert.areequal(3,12/4);
}
testfixture:标记一个类可能具有[test]/[setup]/[teardown]方法,但这个类不能是抽象类。
testfixturesetup:标记在类中所有测试方法执行之前执行的方法。在nunit 2.5之前只能在类中将此标记最多使用于一个实例方法,在nunit 2.5之后则可以标记多个方法,而且不限于实例方法还可以用于静态方法。
testfixtureteardown:标记在类中所有测试方法执行之后再执行的方法。在nunit 2.5之前只能在类中将此标记最多使用于一个实例方法,在nunit 2.5之后则可以标记多个方法,而且不限于实例方法还可以用于静态方法。
timeout:标记被测试的方法最大的执行时间,如果超出标记的时间,则会被取消执行并且被标记为测试失败。
values:标记作为测试方法的一系列的参数。前面的代码实例中就有用法实例。
nunit的断言(assertions)
断言是所有基于xunit单元测试系列的核心,nunit通过nunit.framework.assert类提供了丰富的断言。具体说来,nunit总共提供了11个类别的断言,它们是:
equality asserts:用于断言对象是否相等方面的断言,主要表现为两个方法的重载:assert.areequal()和assert.arenotequal()两种形式的重载,重载参数包括了常见的基本数值类型(int/float/double等)和引用类型(表现为使用object作为参数).
identity asserts:用于判断引用类型的对象是否是同一个引用的断言及断言对象是否存在于某个集合中,如assert.aresame、assert.arenotsame及assert.contains。
condition asserts:用于某些条件的断言,如:assert.istrue、assert.true、assert.isfalse、assert.false、assert.isnull、assert.null、assert.isnotnull、assert.notnull、assert.isnan、assert.isempty及assert.isnotempty。
comparisons asserts:用于数值及实现了icomparable接口的类型之间的断言,如assert.greater(大于)、assert.greaterorequal(大于或等于)、assert.less(小于)、assert.lessorequal(小于或等于)。
type asserts:用于类型之间的判断,比如判断某个实例是否是某一类型或者是从某个类型继承,如:assert.isinstanceoftype、assert.isnotinstanceoftype、assert.isassignablefrom、assert.isnotassignablefrom。在nunit 2.5之后就增加了泛型方法,如assert.isinstanceof<t>、assert.isnotinstanceof<t>、assert.isassignablefrom<t>、assert.isnotassignablefrom<t>。。
exception asserts:有关异常方面的断言,如assert.throws/assert.throws<t>、assert.doesnotthrow、assert.catch/assert.catch<t>。
utility methods:用于精确控制测试过程,总共有四个方法,分别是:assert.pass、assert.fail、assert.ignore、assert.inconclusive。assert.pass和assert.fail是相反的,前者是表示将立即终止测试并将测试结果标识为成功通过测试,后者是立即终止测试并将测试结果标识为测试失败。assert.ignore表示忽略测试,这个标记可以用于标识测试方法或者测试的类。
stringassert:用于字符串方面的断言,提供的方法有stringassert.contains、stringassert.startswith、stringassert.endswith、stringassert.areequalignoringcase及stringassert.ismatch。
collectionassert:关于集合方面的断言,提供的方法有collectionassert.allitemsareinstancesoftype、collectionassert.allitemsarenotnull、collectionassert.allitemsareunique、collectionassert.areequal、collectionassert.areequivalent、collectionassert.arenotequal、collectionassert.arenotequivalent、collectionassert.contains、collectionassert.doesnotcontain、collectionassert.issubsetof、collectionassert.isnotsubsetof、collectionassert.isempty、collectionassert.isnotempty和collectionassert.isordered。
fileassert:用于文件相关的断言,主要提供两个方法:fileassert.areequal和fileassert.arenotequal。
directoryassert:用于文件夹的断言,提供的方法有:directoryassert.areequal、directoryassert.arenotequal、directoryassert.isempty、directoryassert.isnotempty、directoryassert.iswithin和directoryassert.isnotwithin。
nunit的使用
第一次打开nunit时会是一个空白界面,如下图所示:
首先我们需要创建一个nunit项目,点击[file]->[new project]会弹出一个保存nunit项目的对话框,选择合适的路径并输入合适的名称(注意文件后缀名为.nunit),然后点击保存按钮,这样就创建了一个nunit测试项目。以后我们就可以再次打开这个项目了。
此时这个nunit项目中还不包含任何单元测试用例,我们需要创建包含测试用例的项目。打开visual studio创建一个类库项目(在真实项目中通常做法是向当前解决方案中添加类库项目,这样便于解决dll引用问题),接着我们需要添加nunit的引用,这取决于我们是采用安装方式还是免安装方式,通常情况下我们只需要添加对nunit.framework(对应的dll是unit.framework.dll)的引用就够了。
这里周公采用的示例代码如下:
using system;
using system.collections.generic;
using system.linq;
using system.text;
using nunit.framework;
namespace unittestdemo
{
[testfixture]
public class nunittestdemo
{
private ilist<int> intlist = new list<int>();
[setup]
[category("na")]
public void beforetest()
{ console.writeline("beforetest"); }
[testfixturesetup]
[category("na")]
public void beforealltests()
{ console.writeline("beforealltests"); }
[teardown]
[category("na")]
public void aftertest()
{ console.writeline("aftertest"); }
[testfixtureteardown]
[category("na")]
public void afteralltests()
{ console.writeline("afteralltests"); }
[test]
[category("na")]
public void test1()
{ console.writeline("test1"); }
[test]
[category("na")]
public void test2()
{ console.writeline("test2"); }
[test]
public void testfloat()
{
float value = 0.9999999999999999999999999999f;
//value = 0.9999999999999999999999999999;
console.writeline("float value:" + value);
assert.areequal(value, 1f);
console.writeline("testfloat");
}
[test]
public void testdouble()
{
double value = 0.9999999999999999999999999999d;
console.writeline("double value:" + value);
assert.areequal(value, 1d);
console.writeline("test2");
}
[test]
public void testdecimal()
{
decimal value = 0.9999999999999999999999999999m;
console.writeline("decimal value:" + value);
assert.areequal(value, 1m);
console.writeline("test2");
}
[test,repeat(3)]
public void testintlist2()
{
assert.areequal(0, intlist.count);
}
[test]
public void testintlist1()
{
intlist.add(1);
assert.areequal(1, intlist.count);
}
[testcase(12, 3, 4)]
[testcase(12, 2, 6)]
[testcase(12, 4, 3)]
public void dividetest(int n, int d, int q)
{
assert.areequal(q, n / d);
}
[test, combinatorial,description("this is used for show combinatorial")]
public void mytest(
[values(1, 2, 3)] int x,
[values("a", "b")] string s)
{
string value = x + s;
assert.greater(2, value.length);
}
[test]
public void testdemo1(
[values(1, 2, 3)] int x,
[random(-10,10,2)] int y)
{
assert.greater(x + y, 0);
}
[test]
public void testdemo2(
[range(0, 11, 4)] int x)
{
assert.areequal(x%3,0);
}
}
}
编译项目生成dll。我们就可以在nunit主界面上点击[project]->[add assembly...]来添加刚才编译生成的dll,加载成功后界面如下所示:
点击界面上的[run]按钮就可以开始测试了。注意这种方式下是测试所有的测试方法,如果我们只想测试某几个方法,可以勾选方面前面的复选框(默认情况下复选框不出现,需要按照点击[tools]->[setting]打开设置界面,然后点击在[gui]下面找到[tree display],勾选上“show checkboxes”即可)。
如果我们只是想单独测试某个方法,那就更简单了——直接双击那个测试方法即可。
有时候我们进行测试时还会用到一些config文件里面的配置信息,如在app.config/web.config中保存连接字符串信息及其他的配置信息,为了能让nunit测试时能读取app.config/web.config中保存的配置信息,我们需要对nunit进行配置。
为了演示,我们制定以下信息:
项目名称:unittestdemo
项目位置:d:\blogcode\unittestdemo\
项目编译模式(debug/release):debug
为了演示刚才的如何对config文件中保存的数据进行测试,我们在刚才的代码基础上编写了三个测试用例,代码如下:
[test]
public void test0_51ctoblog()
{
stringassert.areequalignoringcase(configurationmanager.appsettings["51ctoblog"], "http://zhoufoxcn.blog.51cto.com");
}
[test]
public void test0_csdnblog()
{
stringassert.areequalignoringcase(configurationmanager.appsettings["csdnblog"], "http://blog.csdn.net/zhoufoxcn");
}
[test]
public void test0_sinaweibo()
{
stringassert.areequalignoringcase(configurationmanager.appsettings["sinaweibo"], "http://weibo.com/zhoufoxcn");
}
同时在app.config文件的appsettings节点增加以下数据:
<appsettings>
<add key="51ctoblog" value="http://zhoufoxcn.blog.51cto.com"/>
<add key="csdnblog" value="http://blog.csdn.net/zhoufoxcn"/>
<add key="sinaweibo" value="http://weibo.com/zhoufoxcn"/>
</appsettings>
如果不在nunit上做任何设置,我们会得到错误的结果,如下图所示:
这时,我们可以按照如下步骤配置,点击[project]-[edit...]打开如下界面:
在上图的界面中设置applicationbase为当前要测试的dll所在的路径,本例中为:d:\blogcode\unittestdemo\bin\debug(注意如果复制全路径到文本框中nunit会自动更改为相对路径),因为当前项目是名为unittestdemo的类库项目,所以对应config文件名称为unittestdemo.dll.config,将其填入configuration file name后面的文本框中,然后我们再次点击[run]按钮就会看到测试通过。
总结
作为xunit体系中的一员,nunit确实给.net开发人员进行单元测试带来了不少方便,在早期我们一直都是使用nunit进行单元测试的。但是也存在着一些不足之处,比如:1.在xunit体系中的junit是在测试每个方法时都是新生成一个实例,而在nunit中确实一个testfixture只会生成一个实例,这样一来如果对要包含单元测试类中的实例数据进行更改会可能会影响到其它的测试方法(像junit那样每次都生成一个实例则不会产生这种情况)。2.早期大多数人以为像junit中一样,[setup]、[teardown]只会在所有测试前、后分别执行一次,实际情况是在每个测试前、后都会执行一次,为了达到junit中[setup]、[teardown]这样的效果,只能新增testfixturesetup、testfixtureteardown属性。除此之外,还存在一些缺点和不足。