单元测试之道

 

作为一个程序员,可能最基本的工作大概就如我一样,年复一年,日复一日的敲打着键盘,编写着用相同的字母组成的各种好像能实现不同的功能的代码。不过,大多的时候,我们也不知道这些的ABC组成的东东是否真的能如我们所愿,可以成功的完成指令。对于一个稍微大一点的项目来说,对那些每天编写的每个小小的零件串联在一起,预测它们可以成为一个庞大的并且实用的工具,的确是一件很困难得事情,更何况,我还曾经看到过(其实在很长的一段时间,我也是)某些人编写了一个几句话的涵数,然后用几分甚至几分钟的时间去重新编译几百兆的程序,然后一步一步调试程序,目的只是为了证明这个涵数的确是可以进行使用的。可惜,即使这样的花费了一天的时间,也无法真的去证明这个方法可以实现我们的目的。

       我们浪费了大量的时间,大量的精力,只因为我们还没有为更好的解决问题做好准备。

幸好,现在有了很多的工具可以帮助我们。于是我们可以大声地喊:啊哈!我们也可以如此简单的进行单元测试了!

       NUnit。对,今天我们就要讨论和学习这一款工具。通过对它的学习和使用,提高我们的代码质量和对测试技术的了解掌握。

       为何要进行单元测试,我想就不必多说了。只要是懒惰的不想提高自己能力的人,都会找出壹千壹万个理由来。所以,我们的探讨只局限于那些不疲不倦的,不时的想提高自己的人。那么,我们如何理解单元测试哪,简单来说,单元测试就是程序员编写的一段代码,用来执行另一段代码,并确定那段代码是否符合我们的期望。

       实际中,为了达到这个目标,我们一般会使用assertion(断言)

       现在我们开始尝试一个简单的测试开始。

    public class Cmp

     {

         public static int Largest(int[] list)

         {

              int index,max = Int32.MaxValue;

              for (index = 0;index < list.Length -1; index++)

              {

                   if (list[index] > max)

                   {

                       max = list[index];

                   }

              }

 

              return max;

         }

     }

这个类文件中包含了一个Largest方法用来返回一个整数数组的最大值。

 

打开NUnit

 

我们编写测试方法    

[TestFixture]

     public class TestDemo

     {

         public TestDemo()

         {

              //

              // TODO: 在此处添加构造函数逻辑

              //

         }

        

         public void IsTrue(bool condition)

         {

              if (!condition)

              {

                  

              }

         }

 

         [Test]

         public void LargestOf()

         {

              Assert.AreEqual(9,Cmp.Largest(new int[]{8,9,7}));

         }

 

         [Test]

         public void LargestOf3Alt()

         {

              int[] arr = new int[3];

              arr[0] = 8;

              arr[1] = 9;

              arr[2] = 7;

              Assert.AreEqual(9,Cmp.Largest(arr));

         }

 

     }

然后在NUnit中加载这个需要测试的DLL

 

左边会显示这个测试类的方法结构 点击要运行的测试方法 在点击RUN

 

我们可以看到图片显示所有方法的指示灯为红色,那就是意味着测试没有通过。如果通过应该显示为了绿色,是黄色的时候表示有些测试被忽略。

 

测试为红色,输出的值不是9,是一个大数字。我们找寻原因,原来

int index,max = Int32.MaxValue;

这里附值出错,我们修改为

Int indexmax = 0

 

测试通过。

为了达到真正的测试的有效性,我们要尝试测试的全面性。当然,对于测试来说,可能要做到100%的全面角度测试根本就是不可能的。简单的说,想一下验证是否是一个有效的三角形需要多少种测试。我想,最有经验的程序员,也会遗漏。

 

我们在测试的方法里面在增加一个测试用例   

public void LargestOf()

         {

              Assert.AreEqual(9,Cmp.Largest(new int[]{8,9,7}));

              Assert.AreEqual(9,Cmp.Largest(new int[]{9,8,7}));

              Assert.AreEqual(9,Cmp.Largest(new int[]{7,8,9}));

         }

运行Run 出现错误

 

出现错误的原因来源自

for (index = 0;index < list.Length -1; index++){}

 应该修改成

for (index = 0;index <= list.Length -1; index++)

OR

for (index = 0;index < list.Length; index++)

在循环条件中,很容易发生这种off-by-one的错误。

 

我们还可以增加对重复的值的测试,一个元素的测试还有包含负数的测试

     [Test]

         public void TestDups()

         {

              Assert.AreEqual(9,Cmp.Largest(new int[]{9,7,9,8}));

         }

 

         [Test]

         public void TestOne()

         {

              Assert.AreEqual(1,Cmp.Largest(new int[]{1}));

         }

 

         [Test]

         public void testNegative()

         {

              int[] negList = new int[]{-9,-8,-7};

              Assert.AreEqual(7,Cmp.Largest(negList));

         }

运行测试结果是

 

负数测试出现问题

原因是max = 0;出现的问题

我们修改为max=Int32.MinValue;

 

我们还可以增加对异常的处理

public static int Largest(int[] list)

         {

              int index,max = Int32.MinValue;

 

              if(list.Length == 0)

              {

                   throw new ArgumentException("数组为空");

              }   

 

              for (index = 0;index <= list.Length -1; index++)

              {

                   if (list[index] > max)

                   {

                       max = list[index];

                   }

              }

 

              return max;

         }

和测试用方法

          [Test,ExpectedException(typeof(ArgumentException))]

         public void TestEmpty()

         {

              Cmp.Largest(new int[]{});

         }

 

具体我们将在后面讨论

 

测试代码是我们用来测试所开发的函数是否被它所宣称的那样达到目的。但是,测试代码仅限于我们开发使用,所以最终它不会和产品一起跑到顾客那里,这就通常意味着我们的测试代码一般是写在一个独立的项目中。一个自己的程序集。

那么测试代码必须要做的几件事:

1:准备测试所需要的各种条件(创建所有必须的对象,分配必要的资源等)

2:调用要测试的方法。

3:验证被测试的方法的行为和期望是否和我们的一致。

4:完成后清理各种资源

 

NUnit的各种断言(assert NUnit提供的帮助我们用来进行测试的函数

1AreEquals

Assert.AreEqual(expected,actual[,string message])

这个是使用最多的断言形式,其中expected 是你期望的值,actual是测试代码产生的值,

Message是一个可选的参数,如果提供了这个参数,将会在发生错误的时候报告这个消息。我们上面的测试例子就是使用这个断言。但是针对比较浮点数的时候要设定将精确到多少位

例子:Assert.AreEqual(3.33, 10.0/3.0, 0.01, “Wanted 3 1/3”)

 

2:IsNull

Assert.IsNull(object [,string message])

Assert.IsNotNull(object [,string message])

验证一个给定的对象是否为null或者非null

 

3:AreSame

Assert.AreSame(expected, actual [, string message])

验证expected参数 actual所引用的是否为同一个对象

 

4:IsTrue

Assert.IsTrue(bool condition [, string message])

验证给定的二元条件是否为真

也可以使用

Assert.IsFalse(bool condition[, string message])

 

5:Fail

Assert.Fail([string message])

使测试立刻失败

 

使用NUnit框架

1:在测试项目中引入NUnit DLL 然后在类文件中

Using NUnit.Framework;

2: 每个包含测试的类必须加上

       [TestFixture] attribute 标记

       测试类必须是Public

3:每个测试类的测试方法必须加上

       [Test] attribute 标记

 

       在测试中,有时候,我们可以希望把一些不同程序员或者不同的测试类放在一个测试用例中进行。这个时候,我们可以使用

[Suite] attribute 标记把这些的测试放入一个 TestFixture集合里,要引入NUnit.Core

 

然后

[TestFixture]

     public class testClassSuite

     {

         [Stuit]

         public static TestSuite Suite

         {

              get

              {

                   TestSuite suite = new TestSuite("Name of Suite");

                   suite.Add(new DatebaseTests()); //增加一个测试类

                   suite.Add(new FredsTests()); //添加的测试类

 

                   return suite;

              }

         }

     }

 

这样可以把 几个想共同执行的测试类放在一起去执行

 

 

分类 Categories

NUint category的概念提供了标记和运行一个个单独的测试和fixture的简单方法。

[TestFixture]

     public class TestShorttestPath

     {

         [Test]

         [Category("Short")]

         public void UserCities()

         {

              int[] negList = new int[]{-9,-8,-7};

              Assert.AreEqual(-7,Cmp.Largest(negList));

         }

        

         [Test,Category("Short")]

         public void UserCitiesTest()

         {

              int[] negList = new int[]{-9,-8,-7,5};

              Assert.AreEqual(-7,Cmp.Largest(negList));

         }

 

         [Test,Category("Long")]

         public void UserCitiesDemo()

         {

              int[] negList = new int[]{3,-8,-7,5};

              Assert.AreEqual(-7,Cmp.Largest(negList));

         }

     }

 

这样运行会在UNint上看到

 

 

 

 

可以对不同的category 进行测试

也可以把整个TestFixture 标志成Category

 

Per-methodSetup Teardown

       每个测试的运行都应该是相互独立的,从而你就可以在任何时候以任意的顺序运行每个单独的测试。为了达到这个目的,在每个测试开始以前,可能需要重新‘

设置测试环境NUint2Attribute 分别用来对环境的建立和清理

[SetUp]

Public void MySetup()

{}

 

[TearDown]

Public void MyTeardown()

{}

比如我们对某个测试要打开某个资源或者数据库,然后测试完成后在释放。

 

Per-class Setup Per-class Teardown

当需要对整个test class设置一些环境的时候 使用这个attribute

[TestFixtureSetUp]

Public void OneTimeSetup()

{}

 

[TestFixtureTearDown]

Public void OneTimeTeardown()

{}

 

自定义Nunit断言

 

NUnit和异常

1:从测试代码抛出的可预测异常

2:由于某个模块或代码发生严重错误,而抛出的不可预测的异常

 

我们已经会用NUnit编写一些简单的测试代码了,但是我们如何去开始自己的测试,并更好的实现自己的测试目标?这-也许才是最重要的。

记得以前刚开始接触单元测试的时候,有一位大师是这么说的:“编写测试,运行使其不通过,编写代码,运行,使测试通过。。。”

那么,我们在软件开发的时候,要努力的去习惯于对每个功能编写有效的测试Case.特别是当我们发现了新的或者别人的功能的Bug的时候,一定要学会用Case来证明。不要只会大声的说,你或者这个有Bug.而要说,现在这里有一个问题,让我们去修改问题使Case通过吧

在真实的测试过程中,可能会出现一些额外的问题。譬如我开始使用Nunit的时候,我要测试一个Rule(业务规则层)的一个方法。当我创建了一个测试的Case,并引入了ERPRuleDLL的时候,发现测试无法通过,并且无法通过的原因是低层连接无法实现,开始我很困惑,不知道发生么什么事情。因为DataAccess曾(数据访问曾)的功能是完整的且经过验证的。并且引用也没有问题,经过仔细检查才发现,其实问题很简单,我的数据连接串是通过XML文件app.Config 来创建的。我在建立测试Case项目的时候,只关注于这个测试的内容,却忽略了读取连接的问题,测试项目读取的连接其实是这个测试项目自身的app.config文件。一个很简单的问题,因为疏忽而产生,其实作为程序员来说,大部分的问题都是由于思考的不周全。就是我们常说的低级错误。

posted on 2006-07-03 10:30  蔡劲松  阅读(394)  评论(0编辑  收藏  举报