【ITPUB 专稿】参看前几篇文章:
新版.Net开发必备十大工具综述篇
新.Net开发必备十大工具之Snippet Compiler
在新版.Net开发必备十大工具一文中,笔者整理总结了.NET平台下开发必备的十大工具,在本篇文章中,我将详细介绍单元测试工具NUnit。NUnit是从Java平台下非常著名的单元测试工具JUnit移植过来的,它是一个免费并且开源的项目。它为我们提供了一套单元测试框架和一个可视化的测试运行程序。
大家可以到NUnit官方主页http://www.nunit.org去下载最新版本,本文使用的是NUnit 2.4.8版本。
认识NUnit
NUnit的可视化工具运行后界面如下图所示:
在面板的中间我们可以看到测试的进度条(或者叫状态条),这里会有三种不同的信号:
绿色表示所有的测试用例都通过;红色表示测试用例中有失败;黄色表示有些测试用例忽略,但测试过的没有失败。 在进度条的上方会有一些统计信息,它们所表示的意义如下:
Test Cases:表示加载的所有测试用例的个数
Tests Run:表示已经运行的测试用例个数
Failures:表示到目前位置运行失败的测试用例个数
Ignored:表示忽略的测试用例个数
Run Time:表示运行所有测试用例所花费的时间
至于Run和Stop按钮我想不用介绍大家都知道是用来干什么的了。
开始第一个测试
NUnit框架是基于Attribute的,这和VSTS是一致的,但它们之间所使用的Attribute并不相同。我们现在编写一个简单的NUnit测试示例,如有下面这样一段代码:
{
public int Add(int a, int b)
{
return a + b;
}
}
我们现在要对Add方法编写单元测试,在开始之前,需要添加对nunit.framework的引用,如下图所示:

NUnit中用到的Attribute都定义在该程序集中,在CalculatorTest中引入命名空间:
using NUnit.Framework;
编写测试类,在NUnit中每个测试类必须加上TestFixture特性,如下代码所示:
public class CalculatorTest
{
}
现在来编写TestAdd测试函数,NUnit中每个测试函数需要加上Test特性,如下代码所示,这里我们添加了两个断言,一是假定创建的对象不为空,二测试Add方法是否返回我们预期的结果:
public void TestAdd()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = 5;
int actualResult = cal.Add(2,3);
Assert.AreEqual(expectedResult, actualResult);
}
OK,至此一个完整的测试用例编写完成,我们使用NUnit可视化工具打开该程序集后,点击Run按钮,效果如下图所示:

全是绿灯表示我们的测试通过,现在再修改一下Calculator类,我们在编写代码时一时大意把Add的实现写成了如下代码:
{
public int Add(int a, int b)
{
return a - b;
}
}
编译并重新运行测试,运行结果如下图所示:
红灯表示测试失败,并且在提示信息框中,我们可以看到提示信息为:期望的结果是5,而实际的结果是-1。
特性简介
通过上面的一个简单的示例,我们已经知道了如何使用NUnit去做单元测试,在NUnit中提供了大量的Attribute来供我们使用,总结如下:

接下我们对这些特性做一下详细的介绍。
Setup和TearDown
我们再修改一下Calculator类,添加一个Mul的方法,如下代码所示:
{
public int Add(int a, int b)
{
return a + b;
}
public int Mul(int a, int b)
{
return a * b;
}
}
同样要对Mul方法编写单元测试,代码如下所示:
public void TestAdd()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = 5;
int a = 2;
int b = 3;
int actualResult = cal.Add(a,b);
Assert.AreEqual(expectedResult, actualResult);
}
[Test]
public void TestMul()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = 6;
int a = 2;
int b = 3;
int actualResult = cal.Mul(a, b);
Assert.AreEqual(expectedResult, actualResult);
}
大家可能已经看出问题来了,这里的TestAdd和TestMul方法中有不少的代码都是重复的,因此我们能够把它们放在一个公用的方法中呢?答案自然是可以的,这时我们就需要用到Setup和TearDown特性。Setup方法用户初始化测试用例数据,而TearDown方法用来测试用例资源回收。如我们可以修改测试类代码如下所示:
public class CalculatorTest
{
private int a;
private int b;
[TestFixtureSetUp]
public void Initialization()
{
a = 2;
b = 3;
}
[Test]
public void TestAdd()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = 5;
int actualResult = cal.Add(a,b);
Assert.AreEqual(expectedResult, actualResult);
}
[Test]
public void TestMul()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = 6;
int actualResult = cal.Mul(a, b);
Assert.AreEqual(expectedResult, actualResult);
}
}
同样我们可以在TestFixtureTearDown方法中做一些资源回收的工作。细心的朋友注意到我们上面介绍的特性中除了TestFixtureSetup和TestFixtureTearDown外,还有一组Setup和TearDown方法,这两组方法的使用非常相似,如下代码所示:
public void EachSetup()
{
// ......
}
[TearDown]
public void EachTearDown()
{
// ......
}
[TestFixtureSetUp]
public void OneTimeSetup()
{
// ......
}
[TestFixtureTearDown]
public void OneTimeTearDown()
{
// ......
}
它们的作用类似,只不过执行的次数不一样,TestFixtureSetup和TestFixtureTearDown是在类初始化(释放)时执行一次,即在所有测试用例方法执行之前(之后)只执行一次;而Setup和TearDown是在每一个测试用例方法执行之前(之后)执行一次,以上面的代码为例,他们之间的关系图如下图所示:

Ignore和Explicit
在做单元测试时,可能有些测试用例非常耗时,如执行一次测试需要几个小时,这时我们不想看到的。我们希望有些测试用例能够不执行或者有些测试用例只有显示调用时才执行,这时Ignore或Explicit就有了用武之地了。使用Ignore特性我们可以忽略某一个测试用例的执行,而使用Explicit则可以在显示调用时才执行,如下代码所示:
public void TestAdd()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = 5;
int actualResult = cal.Add(a,b);
Assert.AreEqual(expectedResult, actualResult);
}
[Test,Explicit]
public void TestMul()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = 6;
int actualResult = cal.Mul(a, b);
Assert.AreEqual(expectedResult, actualResult);
}
现在执行测试用例后,可以看到黄色信号灯出现了:
ExpectedException
在很多时候,我们可能只定义了一个方法,并没有对它进行实现,而只是抛出一个我们已知的异常,我们自然不希望因为这个导致测试用例无法通过。此时ExpectedException可以派上用场了。我们在Calculator在添加一个Sub方法,这里对该方法并不进行实现,而只是抛出一个NullReferenceException异常,如下代码所示:
{
throw new NullReferenceException();
}
现在再编写一个测试用例函数,如下代码所示:
public void TestSub()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = -1;
int actualResult = cal.Sub(a, b);
Assert.AreEqual(expectedResult, actualResult);
}
Sub方法将会抛出一个异常,但我们在TestSub中指定了期望抛出的异常NullReferenceException,所以该测试用例仍然能够通过,如下图所示:
通常而言,我们需要对于期望抛出异常的测试函数写一个专门的测试,来确认该方法在应该抛出异常的地方,确实会抛出我们所期望的异常,对于那些在代码执行中抛出的异常,NUnit会作为测试不通过处理,这一点我们无须担心。
Category
NUnit用Category的概念提供了标记和运行一个个单独的测试和Fixture的简单方法。使用Category,我们可以通过指定一个名称来把一组方法关联起来,然后在运行时有选择的进行测试。如下面的测试代码:
public void TestAdd()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = 5;
int actualResult = cal.Add(a,b);
Assert.AreEqual(expectedResult, actualResult);
}
[Test, Category("Normal")]
public void TestMul()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = 6;
int actualResult = cal.Mul(a, b);
Assert.AreEqual(expectedResult, actualResult);
}
[Test,Category("Long"),ExpectedException(typeof(NullReferenceException))]
public void TestSub()
{
Calculator cal = new Calculator();
Assert.IsNotNull(cal);
int expectedResult = -1;
int actualResult = cal.Sub(a, b);
Assert.AreEqual(expectedResult, actualResult);
}
我们可以把一组执行较长时间的测试函数放在一组中,然后有选择的执行,如下图所示:

断言
在前面的代码中,大家都看到了,NUnit为我们提供了一组辅助函数用于确定某个被测试函数是否工作正常,我们会把这些函数统称为断言。这些断言包括测试某个条件是否为真,两个数据是否相等、不等。如下面两组代码:
判断对象是否为空:
Assert.IsNull(obj);
// 对象不为空
Assert.IsNotNull(obj);
判断对象是否相等:
// 对象相等
Assert.AreEqual(expectedResult, actualResult);
// 对象不相等
Assert.AreNotEqual(expectedResult, actualResult);
通常而言,使用NUnit中提供的默认断言已经足够我们使用了,但是如何你需要对一些特殊数据类型进行测试,则需要进行自定义断言,自定义断言并没有什么特别的地方,与我们编写普通的Class并没有什么区别,只是定义一些静态方法,如下面的代码片段所示:
{
public static void AreEqual(MyClass expected, MyClass result)
{
// ......
}
}
总结
本文详细介绍了新版.NET开发必备十大工具之中的NUnit基本使用方法,但并没有涉及更深的内容Mock等,希望通过本文能够带领你走上单元测试之路。