Nunit单元测试

NUnit是.net平台上使用得最为广泛的测试框架之一,本文将通过示例来描述NUnit的使用方法,并提供若干编写单元测试的建议和技巧,供单元测试的初学者参考。

继续下文之前,先来看看一个非常简单的测试用例(TestCase):

 [Test]

 public void AdditionTest()

 {

     int expectedResult = 2;

 

     Assert.AreEqual(exptectedResult, 1 + 1);

}

你肯定会说这个TestCase也太白痴了吧!这也是许多NUnit文档被人诟病的一点,但是我的理解并不是这样,xUnit本来就是编写UT的简易框 架,keep it simple and stupid,任何通过复杂的TestCase来介绍NUnit的用法都是一种误导,UT复杂之处在于如何在实际项目中应用和实施,而不是徘徊于该如何使 用NUnit。

主要内容:
1、NUnit的基本用法
2、测试用例的组织
3、NUnit的断言(Assert)
4、常用单元测试工具介绍

一、NUnit的基本用法 和 其他xNUnit框架不同的是,NUnit框架使用Attribute(如前面代码中的[Test])来描述测试用例的,也就是说我们只要掌握了Attribute的用法,也就基本学会如何使用NUnit了。VSTS所集成的单元测试也支持类似NUnit的Attributes,下表对比了NUnit和VSTS的标记:

usage

NUnit            attributes

VSTS            attributes

标识测试类

TestFixture

TestClass

标识测试用例(TestCase)

Test

TestMethod

标识测试类初始化函数

TestFixtureSetup

ClassInitialize

标识测试类资源释放函数

TestFixtureTearDown

ClassCleanup

标识测试用例初始化函数

Setup

TestInitialize

标识测试用例资源释放函数

TearDown

TestCleanUp

标识测试用例说明

N/A

Description

标识忽略该测试用例

Ignore

Ignore

标识该用例所期望抛出的异常

ExpectedException

ExpectedException

标识测试用例是否需要显式执行

Explicit

?

标识测试用例的分类

Category

?

 现在,让我们找一个场景,通过示例来了解上述NUnit标记的用法。来看看一个存储在数据库中的数字类:

Nunit单元测试

这是我们常见的DAL+Entity的设计,DigitDataProvider和Digit类的实现代码如下:

1)Digit.cs类:

using System;

using System.Data;

 

 namespace Product

 {

     /// <summary>

     /// Digit 的摘要说明

     /// </summary>

     /// 创 建 人: 罗旭成

     /// 创建日期: 2013-10-22

     /// 修 改 人: 

     /// 修改日期:

     /// 修改内容:

     /// 版    本:

     public class Digit

     {

         private Guid _digitId;

         public Guid DigitID

         {

             get { return this._digitId; }

             set { this._digitId = value; }

         }



         private int _value = 0;

         public int Value

         {

             get { return this._value; }

             set { this._value = value; }

         }

 

         #region 构造函数

         /// <summary>

         /// 默认无参构造函数

         /// </summary>

         /// 创 建 人: 罗旭成

         /// 创建日期: 2013-10-22

         /// 修 改 人: 

         /// 修改日期:

         /// 修改内容:

         public Digit()

         {

             //

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

             //

         }

 

         /// <summary>

         /// construct the digit object from a datarow

         /// </summary>

         /// <param name="row"></param>

         public Digit(DataRow row)

         {

             if (row == null)

             {

                 throw new ArgumentNullException();

             }

 

             if (row["DigitID"] != DBNull.Value)

             {

                 this._digitId = new Guid(row["DigitID"].ToString());

             }

 

             if (row["Value"] != DBNull.Value)

             {

                 this._value = Convert.ToInt32(row["Value"]);

             }

         }

        

         #endregion

     }

 }

2)DigitDataProvider类:

 using System;

 using System.Data;

 using System.Data.SqlClient;

 using System.Collections;

   

 namespace Product

 {

       /// <summary>

       /// DigitDataProvider 的摘要说明

      /// </summary>

      /// 创 建 人: 罗旭成

      /// 创建日期: 2013-10-22

      /// 修 改 人: 

      /// 修改日期:

      /// 修改内容:

      /// 版    本:

      public class DigitDataProvider

      {

          /// <summary>

          /// 定义数据库连接

          /// </summary>

          private SqlConnection _dbConn;

          public SqlConnection Connection

          {

              get { return this._dbConn; }

              set { this._dbConn = value; }

          }

          

          #region 构造函数

          /// <summary>

          /// 默认无参构造函数

          /// </summary>

          /// 创 建 人: 罗旭成

          /// 创建日期: 2013-10-22

          /// 修 改 人: 

          /// 修改日期:

          /// 修改内容:

          public DigitDataProvider()

          {

              //

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

              //

          }

 

          public DigitDataProvider(SqlConnection conn)

          {

              this._dbConn = conn;

          }

          

          #endregion

          

          #region 成员函数定义

  

          /// <summary>

          /// retrieve all Digits in the database

          /// </summary>

          /// <returns></returns>

          public ArrayList GetAllDigits()

          {

              // retrieve all digit record in database

              SqlCommand command = this._dbConn.CreateCommand();

              command.CommandText = "SELECT * FROM digits";

              SqlDataAdapter adapter = new SqlDataAdapter(command);

              DataSet results = new DataSet();

              adapter.Fill(results);

  

              // convert rows to digits collection

              ArrayList digits = null;

  

              if (results != null && results.Tables.Count > 0)

              {

                  DataTable table = results.Tables[0];

                  digits = new ArrayList(table.Rows.Count);

  

                  foreach (DataRow row in table.Rows)

                  {

                      digits.Add(new Digit(row));

                  }

              }

  

              return digits;

          }

  

          /// <summary>

          /// remove all digits from the database

          /// </summary>

          /// <returns></returns>

          public int RemoveAllDigits()

         {

              // retrieve all digit record in database

              SqlCommand command = this._dbConn.CreateCommand();

              command.CommandText = "DELETE FROM digits";

 

             return command.ExecuteNonQuery();

         }

 

          /// <summary>

         /// retrieve and return the entity of given value

         /// </summary>

         /// <exception cref="System.NullReferenceException">entity not exist in the database</exception>

         /// <param name="value"></param>

         /// <returns></returns>

         public Digit GetDigit(int value)

         {

             // retrieve entity of given value

             SqlCommand command = this._dbConn.CreateCommand();

             command.CommandText = "SELECT * FROM digits WHERE Value='" + value + "'";

             SqlDataAdapter adapter = new SqlDataAdapter(command);

             DataSet results = new DataSet();

             adapter.Fill(results);

 

             // convert rows to digits collection

             Digit digit = null;

 

             if (results != null && results.Tables.Count > 0

                 && results.Tables[0].Rows.Count > 0)

             {

                 digit = new Digit(results.Tables[0].Rows[0]);

             }

             else

             {

                 throw new NullReferenceException("not exists entity of given value");

             }

 

             return digit;

         }

 

         /// <summary>

         /// remove prime digits from database

         /// </summary>

         /// <returns></returns>

         public int RemovePrimeDigits()

         {

             throw new NotImplementedException();

         }

 

         #endregion

     }

 }

 

3)新建测试数据库:

CREATE TABLE [dbo].[digits] (

    [DigitID] [uniqueidentifier] NOT NULL ,

    [Value] [int] NOT NULL 

) ON [PRIMARY]

GO

下面,我们开始尝试为DigitDataProvider类编写UT,新建DigitDataProviderTest.cs类。
1、添加nunit.framework引用

Nunit单元测试

并在DigitDataProviderTest.cs中添加:

using NUnit.Framework;

2、编写测试用例
1)标识测试类:NUnit要求每个测试类都必须添加TestFixture的Attribute,并且携带一个public无参构造函数。

 [TestFixture]

 public class DigitProviderTest

 {

     public DigitProviderTest()

     {

     }

 }

2)编写DigitDataProvider.GetAllDigits()的测试函数

 /// <summary>

 /// regular test of DigitDataProvider.GetAllDigits()

 /// </summary>

 [Test]

 public void TestGetAllDigits()

 {

     // initialize connection to the database

     // note: change connection string to ur env

     IDbConnection conn = new SqlConnection(

         "Data source=localhost;user id=sa;password=sa;database=utdemo");

     conn.Open();

 

     // preparing test data

     IDbCommand command = conn.CreateCommand();

     string commadTextFormat = "INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}')";

 

     for (int i = 1; i <= 100; i++)

     {

         command.CommandText = string.Format(

             commadTextFormat, Guid.NewGuid().ToString(), i.ToString());

         command.ExecuteNonQuery();

     }

 

     // test DigitDataProvider.GetAllDigits()

     int expectedCount = 100;

     DigitDataProvider provider = new DigitDataProvider(conn as SqlConnection);

     IList results = provider.GetAllDigits();

 

     // that works?

     Assert.IsNotNull(results);

     Assert.AreEqual(expectedCount, results.Count);

 

     // delete test data

     command = conn.CreateCommand();

     command.CommandText = "DELETE FROM digits";

     command.ExecuteNonQuery();

 

     // close connection to the database

     conn.Close();

 }

什么?很丑?很麻烦?这个问题稍后再讨论,先来看看一个完整的测试用例该如何定义:

 [Test]

 public void TestCase()

 {

     // 1) initialize test environement, like database connection

     

 

     // 2) prepare test data, if neccessary

     

 

     // 3) test the production code by using assertion or Mocks.

     

 

     // 4) clear test data

     

 

     // 5) reset the environment

     

 }

NUnit要求每一个测试函数都可以独立运行(往往有人会误解NUnit并按照Consoler中的排序来执行),这就要求我们在调用目标函数之前先要初 始化目标函数执行所需要的环境,如打开数据库连接、添加测试数据等。为了不影响其他的测试函数,在调用完目标函数后,该测试函数还要负责还原初始环境,如 删除测试数据和关闭数据库连接等。对于同一测试类里的测试函数来说,这些操作往往是相同的,让我们对上面的代码进行一次Refactoring,Extract Method:

 /// <summary>

 /// connection to database

 /// </summary>

 private static IDbConnection _conn;

 

 /// <summary>

 /// 初始化测试类所需资源

 /// </summary>

 [TestFixtureSetUp]

 public void ClassInitialize()

 {

     // note: change connection string to ur env

     DigitProviderTest._conn = new SqlConnection(

         "Data source=localhost;user id=sa;password=sa;database=utdemo");

     DigitProviderTest._conn.Open();

 }

 

 /// <summary>

 /// 释放测试类所占用资源

 /// </summary>

 [TestFixtureTearDown]

 public void ClassCleanUp()

 {

     DigitProviderTest._conn.Close();

 }

 

 /// <summary>

 /// 初始化测试函数所需资源

 /// </summary>

 [SetUp]

 public void TestInitialize()

 {

     // add some test data

     IDbCommand command = DigitProviderTest._conn.CreateCommand();

    string commadTextFormat = "INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}')";

 

     for (int i = 1; i <= 100; i++)

     {

         command.CommandText = string.Format(

             commadTextFormat, Guid.NewGuid().ToString(), i.ToString());

         command.ExecuteNonQuery();

     }

 }

 

 /// <summary>

 /// 释放测试函数所需资源

 /// </summary>

 [TearDown]

 public void TestCleanUp()

 {

     // delete all test data

     IDbCommand command = DigitProviderTest._conn.CreateCommand();

     command.CommandText = "DELETE FROM digits";

 

     command.ExecuteNonQuery();

 }

 

 /// <summary>

 /// regular test of DigitDataProvider.GetAllDigits()

 /// </summary>

 [Test]

 public void TestGetAllDigits()

 {

     int expectedCount = 100;

     DigitDataProvider provider = 

         new DigitDataProvider(DigitProviderTest._conn as SqlConnection);

 

     IList results = provider.GetAllDigits();

     // that works?

     Assert.IsNotNull(results);

     Assert.AreEqual(expectedCount, results.Count);

 }

NUnit提供了以下Attribute来支持测试函数的初始化:
TestFixtureSetup:在当前测试类中的所有测试函数运行前调用;
TestFixtureTearDown:在当前测试类的所有测试函数运行完毕后调用;
Setup:在当前测试类的每一个测试函数运行前调用;
TearDown:在当前测试类的每一个测试函数运行后调用。

3)编写DigitDataProvider.RemovePrimeDigits()的测试函数
唉,又忘了质数判断的算法,这个函数先不实现(throw new NotImplementedException()),对应的测试函数先忽略。

/// <summary>

 /// regular test of DigitDataProvider.RemovePrimeDigits

 /// </summary>

 [Test, Ignore("Not Implemented")]

 public void TestRemovePrimeDigits()

 {

     DigitDataProvider provider = 

         new DigitDataProvider(DigitProviderTest._conn as SqlConnection);

 

     provider.RemovePrimeDigits();

 }

Ignore的用法:

 

Ignore(string reason)

 

4)编写DigitDataProvider.GetDigit()的测试函数
当查找一个不存在的Digit实体时,GetDigit()会不会像我们预期一样抛出NullReferenceExceptioin呢?

 /// <summary>

 /// Exception test of DigitDataProvider.GetDigit()

 /// </summary>

 [Test, ExpectedException(typeof(NullReferenceException))]

 public void TestGetDigit()

 {

     int expectedValue = 999;

     DigitDataProvider provider = 

         new DigitDataProvider(DigitProviderTest._conn as SqlConnection);

 

     Digit digit = provider.GetDigit(expectedValue);

 }

ExpectedException的用法

 

ExpectedException(Type t)

ExpectedException(Type t, string expectedMessage)

 

在NUnitConsoler里执行一把,欣赏一下黄绿灯吧。本文相关代码可从UTDemo_Product.rar下载。

二、测试函数的组织
现在有一个性能测试的Testcase,执行一次要花上一个小时,我们并不需要(也无法忍受)每次自动化测试时都去执行这样的Testcase,使用NUnit的Explicit标记可以让这个TestCase只有在显示调用下才会执行:

 [Test, Explicit]

 public void OneHourTest()

 {

     //

 }

Nunit单元测试

不幸的是,这样耗时的TestCase在整个测试工程中可能有数十个,或许更多,我们能不能把这些TestCase都组织起来,要么一起运行,要么不运行呢?NUnit提供的Category标记可实现此功能:

 [Test, Explicit, Category("LongTest")]

 public void OneHourTest()

 {

     ...

 }

 

 [Test, Explicit, Category("LongTest")]

 public void TwoHoursTest()

 {

     ...

 }

这样,只有当显示选中LongTest分类时,这些TestCase才会执行

Nunit单元测试

三、NUnit的断言
NUnit提供了一个断言类NUnit.Framework.Assert,可用来进行简单的state base test(见idior的Enterprise Test Driven Develop),可别对这个断言类期望太高,在实际使用中,我们往往需要自己编写一些高级断言。
常用的NUnit断言有:

method

usage

example

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

验证两个对象是否相等

Assert.AreEqual(2, 1+1)

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

验证两个引用是否指向同意对象

object expected = new object();

object actual = expected;

Assert.AreSame(expected, actual)

Assert.IsFalse(bool)

验证bool值是否为false

Assert.IsFalse(false)

Assert.IsTrue(bool)

验证bool值是否为true

Assert.IsTrue(true)

Assert.IsNotNull(object)

验证对象是否不为null

Assert.IsNotNull(new object())

Assert.IsNull(object)

验证对象是否为null

Assert.IsNull(null);

这 里要特殊指出的Assert.AreEqual只能处理基本数据类型和实现了Object.Equals接口的对象的比较,对于我们自定义对象的比较,通 常需要自己编写高级断言,这个问题郁闷了我好一会,下面给出一个用于level=1的情况下的对象比较的高级断言的实现:

 public class AdvanceAssert

 {

     /// <summary>

     /// 验证两个对象的属性值是否相等

     /// </summary>

     /// <remarks>

     /// 目前只支持的属性深度为1层

     /// </remarks>

     public static void AreObjectsEqual(object expected, object actual)

     {

         // 若为相同引用,则通过验证

         if (expected == actual)

         {

             return;

         }

 

         // 判断类型是否相同

         Assert.AreEqual(expected.GetType(), actual.GetType());

 

         // 测试属性是否相等

         Type t = expected.GetType();

         PropertyInfo[] properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);

 

         foreach (PropertyInfo property in properties)

         {

             object obj1 = t.InvokeMember(property.Name, 

                 BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty, 

                 null, expected, null);

             object obj2 = t.InvokeMember(property.Name, 

                 BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty, 

                 null, actual, null);

 

             // 判断属性是否相等

             AdvanceAssert.AreEqual(obj1, obj2, "assertion failed on " + property.Name);

         }

     }

 

     /// <summary>

     /// 验证对象是否相等

     /// </summary>

     private static void AreEqual(object expected, object actual, string message)

     {

         Type t = expected.GetType();

 

         if (t.Equals(typeof(System.DateTime)))

         {

             Assert.AreEqual(expected.ToString(), actual.ToString(), message);

         }

         else

         {

             // 默认使用NUnit的断言

             Assert.AreEqual(expected, actual, message);

         }

     }

 }

四、常用单元测试工具介绍:
1、NUnit:目前最高版本为2.6.2(也是本文所使用的NUnit的版本)
下载地址:http://www.nunit.org

2、TestDriven.Net:一款把NUnit和VS IDE集成的插件
下载地址:http://www.testdriven.net/

3、NUnit2Report:和nant结合生成单元测试报告
下载地址:http://nunit2report.sourceforge.net

4、Rhino Mocks 2:个人认为时.net框架下最好的mocks库,而且支持.net 2.0, rocks~!
下载地址:http://www.ayende.com/projects/rhino-mocks.aspx


想不到一口气写了这么多,前段时间在公司的项目中进行了一次单元测试的尝试,感触很深,看了idior的文章后更加觉得单元测试日后会成为项目的必需部分。在后续的文章中,我将讨论mocks,自定义测试框架和自动化测试工具,希望能和园子里的uter多多讨论。

好向往TDD~~

 

你可能感兴趣的:(单元测试)