Nunit單元測試

NUnit是.net平臺上使用得最爲普遍的測試框架之一,本文將經過示例來描述NUnit的使用方法,並提供若干編寫單元測試的建議和技巧,供單元測試的初學者參考。

繼續下文以前,先來看看一個很是簡單的測試用例(TestCase):html

 [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。git

主要內容:
一、NUnit的基本用法
二、測試用例的組織
三、NUnit的斷言(Assert)
四、經常使用單元測試工具介紹算法

1、NUnit的基本用法 和 其餘xNUnit框架不一樣的是,NUnit框架使用Attribute(如前面代碼中的[Test])來描述測試用例的,也就是說咱們只要掌握了Attribute的用法,也就基本學會如何使用NUnit了。VSTS所集成的單元測試也支持相似NUnit的Attributes,下表對比了NUnit和VSTS的標記:數據庫

usage框架

NUnit            attributeside

VSTS            attributes函數

標識測試類工具

TestFixturepost

TestClass性能

標識測試用例(TestCase)

Test

TestMethod

標識測試類初始化函數

TestFixtureSetup

ClassInitialize

標識測試類資源釋放函數

TestFixtureTearDown

ClassCleanup

標識測試用例初始化函數

Setup

TestInitialize

標識測試用例資源釋放函數

TearDown

TestCleanUp

標識測試用例說明

N/A

Description

標識忽略該測試用例

Ignore

Ignore

標識該用例所指望拋出的異常

ExpectedException

ExpectedException

標識測試用例是否須要顯式執行

Explicit

?

標識測試用例的分類

Category

?

 如今,讓咱們找一個場景,經過示例來了解上述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類。
一、添加nunit.framework引用

並在DigitDataProviderTest.cs中添加:

using NUnit.Framework;

二、編寫測試用例
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下載。

2、測試函數的組織
如今有一個性能測試的Testcase,執行一次要花上一個小時,咱們並不須要(也沒法忍受)每次自動化測試時都去執行這樣的Testcase,使用NUnit的Explicit標記可讓這個TestCase只有在顯示調用下才會執行:

 [Test, Explicit]
 public void OneHourTest()
 {
     //
 }

不幸的是,這樣耗時的TestCase在整個測試工程中可能有數十個,或許更多,咱們能不能把這些TestCase都組織起來,要麼一塊兒運行,要麼不運行呢?NUnit提供的Category標記可實現此功能:

 [Test, Explicit, Category("LongTest")]
 public void OneHourTest()
 {
     ...
 }
 
 [Test, Explicit, Category("LongTest")]
 public void TwoHoursTest()
 {
     ...
 }

這樣,只有當顯示選中LongTest分類時,這些TestCase纔會執行

3、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);
         }
     }
 }

4、經常使用單元測試工具介紹:
一、NUnit:目前最高版本爲2.6.2(也是本文所使用的NUnit的版本)
下載地址:http://www.nunit.org

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

三、NUnit2Report:和nant結合生成單元測試報告
下載地址:http://nunit2report.sourceforge.net

四、Rhino Mocks 2:我的認爲時.net框架下最好的mocks庫,並且支持.net 2.0, rocks~!
下載地址:http://www.ayende.com/projects/rhino-mocks.aspx


想不到一口氣寫了這麼多,前段時間在公司的項目中進行了一次單元測試的嘗試,感觸很深,看了idior的文章後更加以爲單元測試往後會成爲項目的必需部分。在後續的文章中,我將討論mocks,自定義測試框架和自動化測試工具,但願能和園子裏的uter多多討論。

好向往TDD~~

相關文章
相關標籤/搜索