在上篇文章中提到过,单元测试是程序员编写的代码,用于验证某段代码的行为是否与开发者所期望的一致,并且说明了它的重要性,现在是时候来看看在实际开发中如何去做了。
为了验证代码行为是否与我们所期望的一致,就需要使用断言(assertion)。断言就是对待测代码的结果进行检查,判断是否与期望的一致。比如下面的IsTrue方法,它检查给定的条件是否为真:如果非真,则断言失败,程序中止。
public void IsTrue(bool condition)
{
if (!condition)
{
Abort();
}
}
比如,有如下代码:
int a = 2;
IsTrue(a == 2);
如果a不等于2,那么程序会退出。不难理解,所有类似的断言都可以使用此方法。但情况却不都像a == 2这样简单,我们要断言的值可能是浮点数、字符串、集合等等很多情况,如果都使用IsTrue,那么在调用前我们都要考虑如何进行比较,而且我们的处理也不止程序中止这种方式,好像很复杂啊。
如果有一个专门处理这些断言的工具多好啊。很幸运,已经有了,我们可以选择NUnit或者MbUnit。我们这里先看看NUnit,它是.NET这边最正统的单元测试工具。NUnit提供了自己的GUI工具,我们可以通过该工具测试,但是有更好的选择——TestDriven.NET,有了它,就不需要在开发环境和测试工具间来回切换了,单元测试在VS内就可以完成。
在NUnit中,有个Assert类,它提供了多种断言方式,可以满足大部分需要了。比如针对两个整数是否相等的断言,可以使用Assert.AreEqual,我们没必要写自己的IsTrue方法了,只要调用:Assert.AreEqual(2, a)即可。
好,现在从一个简单的例子开始,看一下如何使用NUnit进行单元测试。
计划你的测试
我们考虑如下一个方法,它查找一个int数组中的元素的最大值:
static int Largest(int[] array);
给定一个数组[7, 8, 9],该方法应该返回9(这就是我们的期望值)。等一下,你有没有想到其它的测试呢?思考一分钟再往下看。
首先,元素的位置应与返回值无关,所以可想到如下测试:
l [7, 8, 9] -> 9
l [8, 9, 7] -> 9
l [9, 7, 8] -> 9
接下来,如果数组中有两个相等的最大值,该是如何?
l [7, 9, 8, 9] -> 9
还有,如果数组只有一个元素,会是怎样?
l [1] -> 1
别忘了整数包含负数呢:
l [-9, -8, -7] -> -7
还不写代码啊,我早就想到它的实现方法了:
public static int Largest(int[] array)
{
int index, max = Int32.MaxValue;
for (int index = 0; index < array.Length - 1; index++)
{
if (array[index] > max)
{
max = array[index];
}
return max;
}
}
测试一个简单的方法
我们来给上面的方法编写一个测试用例(Test Case):
[TestFixture]
public class MyUtilTest
{
[Test]
public void LargestOf3()
{
Assert.AreEqual(9, MyUtil.Largest(new int[]{8, 9, 7}));
}
}
好,激动人心的时刻到了,运行下看看。
不好,测试没通过,出现了下面的信息
TestCase 'Ch02.MyUtilTest.LargestOf3' failed:
Expected: 9
But was: 2147483647
D:\myWorks\VS2008\Consoles\UnitTestingInAction\Ch02\MyUtilTest.cs(22,0): at Ch02.MyUtilTest.LargestOf3()
结果与期望不一致,输入的是7、8、9,怎么会返回那么大的数字呢?看看代码,max变量的初始值为Int32.MaxValue,这就不对了,如果改成0的话就对了。此时测试确实是通过的。
上面我们想到了很多测试还没做呢,先考虑最大值的位置,这个跟结果应当是无关的。
再添加两个断言,代码还是写在刚才的测试用例中。
Assert.AreEqual(9, MyUtil.Largest(new int[] { 8, 9, 7 }));
Assert.AreEqual(9, MyUtil.Largest(new int[] { 9, 8, 7 }));
Assert.AreEqual(9, MyUtil.Largest(new int[] { 7, 8, 9 }));
现在该通过了,运行测试。
晕,又没通过:
TestCase 'Ch02.MyUtilTest.LargestOf3' failed:
Expected: 9
But was: 8
D:\myWorks\VS2008\Consoles\UnitTestingInAction\Ch02\MyUtilTest.cs(24,0): at Ch02.MyUtilTest.LargestOf3()
第三个断言失败,最大值是8,你的直觉是什么?是不是好像9根本没执行到呢?检查下for循环:
for (index = 0; index < array.Length - 1; index++)
通常我们会写index < array.Length,这里显然少循环了一次,这是个事故多发地带,这个错误被称为“off-by-one”错误,如果使用foreach语句就不存在这个问题了。
现在测试终于通过了,再看看重复最大值和单一元素的断言:
[Test]
public void TestDups()
{
Assert.AreEqual(9, MyUtil.Largest(new int[] { 8, 9, 7, 9 }));
}
[Test]
public void TestOne()
{
Assert.AreEqual(1, MyUtil.Largest(new int[] { 1 }));
}
至此一切正常,Yeah!等等,如果元素是负数呢?
Assert.AreEqual(-7, MyUtil.Largest(new int[] { -9, -8, -7 }));
TestCase 'Ch02.MyUtilTest.TestNegative' failed:
Expected: -7
But was: 0
D:\myWorks\VS2008\Consoles\UnitTestingInAction\Ch02\MyUtilTest.cs(42,0): at Ch02.MyUtilTest.TestNegative()
0是哪里来的?看来又是初始值的问题,0要大于负数,看来初始值必须是一个最小的整数,它就是Int32.MinValue,这样就可以了。
最后,如果array为空(长度为0)怎么办?这个时候最大值是无意义的,返回任何值都不合适,应当抛出一个异常,代码修改如下:
int index, max = Int32.MinValue;
if (array.Length == 0)
{
throw new ArgumentException("largest: Empty array.");
}
for (index = 0; index < array.Length; index++)
…
注意,代码在设计上发生了改动,改善设计正是单元测试的好处之一。
为之编写测试用例:
[Test, ExpectedException(typeof(ArgumentException))]
public void TestEmpty()
{
MyUtil.Largest(new int[] { });
}
注意,这个方法的特性中,除了Test,还多了个ExpectedException,与前面的用例不同,我们期望的正是异常。
小结
本文通过一个简单的例子描述了单元测试的过程,从此我们也可以编写测试用例了,对其有了初步的认识。其中的过程有些繁琐,也许你会问,这么一个简单的方法值得花费这么大的力气吗?答案是肯定的,单元测试保证了程序在当前的质量,而在维护时会体现出更大的价值。
文中用到了NUnit和TestDriven.NET两个工具,其详细用法可以在园子里搜一下,在后续文章中我也不想再写相关内容了。从下一篇开始将逐步深入地讨论单元测试的实施过程。
本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2008/04/14/your-first-unit-test.html,如需转载请自行联系原作者。