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~~