NUnit介紹
NUnit是一個專門針對於.NET來寫的單元測試框架,它是xUnit體系中的一員,在xUnit體系中還有針對Java的JUnit和針對C++的CPPUnit,在開始的時候NUnit和xUnit體系中的大多數的做法一樣,僅僅是將Smalltalk或者Java版本轉換而來,但是在.NET2.0之後它加入了一些特有的做法。NUnit的官方網站是:http://www.nunit.org/,目前的最新版本是:2.6.2。
NUnit下載與安裝
NUnit的每個版本都提供了兩種形式的下載:安裝檔案和免安裝方式,分別是*.msi格式和*.zip格式。前者需要安裝才能使用,並且會在安裝過程中建立一些快捷方式和註冊NUnit的dll到GAC,這樣以後編寫NUnit測試類的時候新增NUnit的dll就像新增.Net Framework的dll一樣。如果是下載的zip格式的檔案,則不會建立快捷方式和註冊dll,在編寫單元測試類時需要手動指定NUnit的dll的路徑。
NUnit的執行有三種方式:命令列和圖形使用者介面。以周公當前電腦上安裝的NUnit2.5.10為例,安裝路徑為:C:\Program Files (x86)\NUnit 2.5.10,其下有三個目錄:bin、doc和samples。在doc目錄下是軟體的文件(英文),在samples目錄下則是一些樣例程式碼。如果是採用免安裝模式的話,執行NUnit就需要執行bin目錄下的檔案,在bin目錄下有net-1.1和net-2.0兩個資料夾,分別對應.net的不同版本。
下面介紹如何以不同的方式啟動NUnit:
命令列模式:執行nunit-console.exe。
圖形使用者介面模式:執行nunit.exe。
並行(parallel)模式:執行pnunit-launcher.exe。
注意:.Net2.0版本的NUnit是使用/platform:anycpu引數來編譯的,我們知道這樣的結果是執行在x86的系統上會被JIT編譯成32位的程式,而在x64的系統上會被JIT編譯成64位的程式。如果使用NUnit在x64系統上測試32位的程式就會帶來問題。為了避免這個問題,可以使用nunit-agent-x86.exe/nunit-x86.exe來測試,因為在編譯的時候使用了/platform:x86作為編譯引數。
下圖是執行NUnit的GUI介面:
NUnit的常用Attribute標記
這些都是可以用來作為類或者方法的屬性,它們都是System.Attribute類的直接或間接子類,有如下:
Category:用來將測試分類。這個在主介面可以看到Tests/Categories兩個選項卡,如果給方法標記了Category屬性就會在Categories選項卡中看得到。
Combinatorial:用來將來測試時需要測試各種可能的組合,比如如下程式碼:
[Test, Combinatorial]
public void MyTest(
[Values(1, 2, 3)] int x,
[Values("A", "B")] string s)
{
string value = x + s;
Assert.Greater(2, value.Length);
}
測試時實際會測試6種情況:MyTest(1, "A")/MyTest(1, "B")/MyTest(2, "A")/MyTest(2, "B")/MyTest(3, "A")/MyTest(3, "B")。
Culture:設定測試時的語言環境,這對我們測試一些語言敏感的場景下有用,比如DateTime.ToString()在不同語言環境下得到的字串並不相同。
Description:用於指定測試對應的描述,如果選擇將測試結果生成XML檔案,那麼就會在XML檔案中看到這些描述。
ExpectedException:指出執行測試時將會丟擲Exception。
Explicit:如果測試的類或者方法使用此Attribute,那麼在使用帶GUI的NUnit測試時這個類或者方法必須在介面上選定才會被執行。
Explicit:忽略某個測試類或者方法。
Maxtime:測試方法最大執行的毫秒數,如果程式的執行時間超過指定數值,那麼就會被認為測試失敗。
Random:用於指定如何隨機生成引數來測試方法。如下面的程式碼:
[Test]
public void TestDemo1(
[Values(1, 2, 3)] int x,
[Random(-10,10,2)] int y)
{
Assert.Greater(x + y, 0);
}
表示方法TestDemo1會生成6個測試,1,2,3分別作為引數x的值與兩次從-10到10之間的隨機數y組成6次測試。
Range:指定引數的方法,如下面的方法:
[Test]
public void TestDemo2(
[Range(0, 11, 4)] int x)
{
Assert.AreEqual(x%3,0);
}
表示從0開始遞增,步長為4,且不大於11。
Repeat:將重複測試的次數。
RequiresMTA:表示測試時需要多執行緒單元(multi-threaded apartment)。
RequiresSTA:表示測試時需要單執行緒單元(single-threaded apartment)。
SetUp:在每個測試方法開始之前執行的初始化操作。在NUnit 2.5之前要求每個類只能有一個帶SetUp屬性的例項方法,但在NUnit 2.5之後則沒有次數和必須是例項方法的限制。
TearDown:與SetUp的作用相反,是在每個測試方法執行結束之後執行的方法。在NUnit 2.5之前要求每個類只能有一個帶SetUp屬性的例項方法,但在NUnit 2.5之後則沒有次數和必須是例項方法的限制。
Test:用來標記需要測試的方法,在NUnit 2.5之前只能用於標記例項方法,在NUnit 2.5之後則可以用於標記靜態方法。
TestCase:標記方法具有引數並且提供了在測試時需要的引數。如下面的程式碼:
[TestCase(12, 3, 4)]
[TestCase(12, 2, 6)]
[TestCase(12, 4, 3)]
public void DivideTest(int n, int d, int q)
{
Assert.AreEqual(q, n / d);
}
將會執行三次測試,相當於:
[Test]
public void DivideTest()
{
Assert.AreEqual(4,12/3);
}
[Test]
public void DivideTest()
{
Assert.AreEqual(6,12/2);
}
[Test]
public void DivideTest()
{
Assert.AreEqual(3,12/4);
}
TestFixture:標記一個類可能具有[Test]/[SetUp]/[TearDown]方法,但這個類不能是抽象類。
TestFixtureSetUp:標記在類中所有測試方法執行之前執行的方法。在NUnit 2.5之前只能在類中將此標記最多使用於一個例項方法,在NUnit 2.5之後則可以標記多個方法,而且不限於例項方法還可以用於靜態方法。
TestFixtureTearDown:標記在類中所有測試方法執行之後再執行的方法。在NUnit 2.5之前只能在類中將此標記最多使用於一個例項方法,在NUnit 2.5之後則可以標記多個方法,而且不限於例項方法還可以用於靜態方法。
Timeout:標記被測試的方法最大的執行時間,如果超出標記的時間,則會被取消執行並且被標記為測試失敗。
Values:標記作為測試方法的一系列的引數。前面的程式碼例項中就有用法例項。
NUnit的斷言(Assertions)
斷言是所有基於xUnit單元測試系列的核心,NUnit通過NUnit.Framework.Assert類提供了豐富的斷言。具體說來,NUnit總共提供了11個類別的斷言,它們是:
Equality Asserts:用於斷言物件是否相等方面的斷言,主要表現為兩個方法的過載:Assert.AreEqual()和Assert.AreNotEqual()兩種形式的過載,過載引數包括了常見的基本數值型別(int/float/double等)和引用型別(表現為使用object作為引數).
Identity Asserts:用於判斷引用型別的物件是否是同一個引用的斷言及斷言物件是否存在於某個集合中,如Assert.AreSame、Assert.AreNotSame及Assert.Contains。
Condition Asserts:用於某些條件的斷言,如:Assert.IsTrue、Assert.True、Assert.IsFalse、Assert.False、Assert.IsNull、Assert.Null、Assert.IsNotNull、Assert.NotNull、Assert.IsNaN、Assert.IsEmpty及Assert.IsNotEmpty。
Comparisons Asserts:用於數值及實現了IComparable介面的型別之間的斷言,如Assert.Greater(大於)、Assert.GreaterOrEqual(大於或等於)、Assert.Less(小於)、Assert.LessOrEqual(小於或等於)。
Type Asserts:用於型別之間的判斷,比如判斷某個例項是否是某一型別或者是從某個型別繼承,如:Assert.IsInstanceOfType、Assert.IsNotInstanceOfType、Assert.IsAssignableFrom、Assert.IsNotAssignableFrom。在NUnit 2.5之後就增加了泛型方法,如Assert.IsInstanceOf<T>、Assert.IsNotInstanceOf<T>、Assert.IsAssignableFrom<T>、Assert.IsNotAssignableFrom<T>。。
Exception Asserts:有關異常方面的斷言,如Assert.Throws/Assert.Throws<T>、Assert.DoesNotThrow、Assert.Catch/Assert.Catch<T>。
Utility Methods:用於精確控制測試過程,總共有四個方法,分別是:Assert.Pass、Assert.Fail、Assert.Ignore、Assert.Inconclusive。Assert.Pass和Assert.Fail是相反的,前者是表示將立即終止測試並將測試結果標識為成功通過測試,後者是立即終止測試並將測試結果標識為測試失敗。Assert.Ignore表示忽略測試,這個標記可以用於標識測試方法或者測試的類。
StringAssert:用於字串方面的斷言,提供的方法有StringAssert.Contains、StringAssert.StartsWith、StringAssert.EndsWith、StringAssert.AreEqualIgnoringCase及StringAssert.IsMatch。
CollectionAssert:關於集合方面的斷言,提供的方法有CollectionAssert.AllItemsAreInstancesOfType、CollectionAssert.AllItemsAreNotNull、CollectionAssert.AllItemsAreUnique、CollectionAssert.AreEqual、CollectionAssert.AreEquivalent、CollectionAssert.AreNotEqual、CollectionAssert.AreNotEquivalent、CollectionAssert.Contains、CollectionAssert.DoesNotContain、CollectionAssert.IsSubsetOf、CollectionAssert.IsNotSubsetOf、CollectionAssert.IsEmpty、CollectionAssert.IsNotEmpty和CollectionAssert.IsOrdered。
FileAssert:用於檔案相關的斷言,主要提供兩個方法:FileAssert.AreEqual和FileAssert.AreNotEqual。
DirectoryAssert:用於資料夾的斷言,提供的方法有:DirectoryAssert.AreEqual、DirectoryAssert.AreNotEqual、DirectoryAssert.IsEmpty、DirectoryAssert.IsNotEmpty、DirectoryAssert.IsWithin和DirectoryAssert.IsNotWithin。
NUnit的使用
第一次開啟NUnit時會是一個空白介面,如下圖所示:
首先我們需要建立一個NUnit專案,點選[File]->[New Project]會彈出一個儲存NUnit專案的對話方塊,選擇合適的路徑並輸入合適的名稱(注意檔案字尾名為.nunit),然後點選儲存按鈕,這樣就建立了一個NUnit測試專案。以後我們就可以再次開啟這個專案了。
此時這個NUnit專案中還不包含任何單元測試用例,我們需要建立包含測試用例的專案。開啟Visual Studio建立一個類庫專案(在真實專案中通常做法是向當前解決方案中新增類庫專案,這樣便於解決dll引用問題),接著我們需要新增NUnit的引用,這取決於我們是採用安裝方式還是免安裝方式,通常情況下我們只需要新增對nunit.framework(對應的dll是unit.framework.dll)的引用就夠了。
這裡周公採用的示例程式碼如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using NUnit.Framework;
- namespace UnitTestDemo
- {
- [TestFixture]
- public class NUnitTestDemo
- {
- private IList<int> intList = new List<int>();
- [SetUp]
- [Category("NA")]
- public void BeforeTest()
- { Console.WriteLine("BeforeTest"); }
- [TestFixtureSetUp]
- [Category("NA")]
- public void BeforeAllTests()
- { Console.WriteLine("BeforeAllTests"); }
- [TearDown]
- [Category("NA")]
- public void AfterTest()
- { Console.WriteLine("AfterTest"); }
- [TestFixtureTearDown]
- [Category("NA")]
- public void AfterAllTests()
- { Console.WriteLine("AfterAllTests"); }
- [Test]
- [Category("NA")]
- public void Test1()
- { Console.WriteLine("Test1"); }
- [Test]
- [Category("NA")]
- public void Test2()
- { Console.WriteLine("Test2"); }
- [Test]
- public void TestFloat()
- {
- float value = 0.9999999999999999999999999999f;
- //value = 0.9999999999999999999999999999;
- Console.WriteLine("float value:" + value);
- Assert.AreEqual(value, 1f);
- Console.WriteLine("TestFloat");
- }
- [Test]
- public void TestDouble()
- {
- double value = 0.9999999999999999999999999999d;
- Console.WriteLine("double value:" + value);
- Assert.AreEqual(value, 1d);
- Console.WriteLine("Test2");
- }
- [Test]
- public void TestDecimal()
- {
- decimal value = 0.9999999999999999999999999999M;
- Console.WriteLine("decimal value:" + value);
- Assert.AreEqual(value, 1M);
- Console.WriteLine("Test2");
- }
- [Test,Repeat(3)]
- public void TestIntList2()
- {
- Assert.AreEqual(0, intList.Count);
- }
- [Test]
- public void TestIntList1()
- {
- intList.Add(1);
- Assert.AreEqual(1, intList.Count);
- }
- [TestCase(12, 3, 4)]
- [TestCase(12, 2, 6)]
- [TestCase(12, 4, 3)]
- public void DivideTest(int n, int d, int q)
- {
- Assert.AreEqual(q, n / d);
- }
- [Test, Combinatorial,Description("This is used for show Combinatorial")]
- public void MyTest(
- [Values(1, 2, 3)] int x,
- [Values("A", "B")] string s)
- {
- string value = x + s;
- Assert.Greater(2, value.Length);
- }
- [Test]
- public void TestDemo1(
- [Values(1, 2, 3)] int x,
- [Random(-10,10,2)] int y)
- {
- Assert.Greater(x + y, 0);
- }
- [Test]
- public void TestDemo2(
- [Range(0, 11, 4)] int x)
- {
- Assert.AreEqual(x%3,0);
- }
- }
- }
編譯專案生成dll。我們就可以在NUnit主介面上點選[Project]->[Add Assembly...]來新增剛才編譯生成的dll,載入成功後介面如下所示:
點選介面上的[Run]按鈕就可以開始測試了。注意這種方式下是測試所有的測試方法,如果我們只想測試某幾個方法,可以勾選方面前面的核取方塊(預設情況下核取方塊不出現,需要按照點選[Tools]->[Setting]開啟設定介面,然後點選在[GUI]下面找到[Tree Display],勾選上“Show CheckBoxes”即可)。
如果我們只是想單獨測試某個方法,那就更簡單了——直接雙擊那個測試方法即可。
有時候我們進行測試時還會用到一些config檔案裡面的配置資訊,如在app.config/web.config中儲存資料庫連線字串資訊及其他的配置資訊,為了能讓NUnit測試時能讀取app.config/web.config中儲存的配置資訊,我們需要對NUnit進行配置。
為了演示,我們制定以下資訊:
專案名稱:UnitTestDemo
專案位置:D:\BlogCode\UnitTestDemo\
專案編譯模式(Debug/Release):Debug
為了演示剛才的如何對config檔案中儲存的資料進行測試,我們在剛才的程式碼基礎上編寫了三個測試用例,程式碼如下:
- [Test]
- public void Test0_51CTOBlog()
- {
- StringAssert.AreEqualIgnoringCase(ConfigurationManager.AppSettings["51ctoBlog"], "http://zhoufoxcn.blog.51cto.com");
- }
- [Test]
- public void Test0_CSDNBlog()
- {
- StringAssert.AreEqualIgnoringCase(ConfigurationManager.AppSettings["CSDNBlog"], "http://blog.csdn.net/zhoufoxcn");
- }
- [Test]
- public void Test0_SinaWeiBo()
- {
- StringAssert.AreEqualIgnoringCase(ConfigurationManager.AppSettings["SinaWeiBo"], "http://weibo.com/zhoufoxcn");
- }
同時在app.config檔案的appSettings節點增加以下資料:
- <appSettings>
- <add key="51ctoBlog" value="http://zhoufoxcn.blog.51cto.com"/>
- <add key="CSDNBlog" value="http://blog.csdn.net/zhoufoxcn"/>
- <add key="SinaWeiBo" value="http://weibo.com/zhoufoxcn"/>
- </appSettings>
如果不在NUnit上做任何設定,我們會得到錯誤的結果,如下圖所示:
這時,我們可以按照如下步驟配置,點選[Project]-[Edit...]開啟如下介面:
在上圖的介面中設定ApplicationBase為當前要測試的dll所在的路徑,本例中為:D:\BlogCode\UnitTestDemo\bin\Debug(注意如果複製全路徑到文字框中NUnit會自動更改為相對路徑),因為當前專案是名為UnitTestDemo的類庫專案,所以對應config檔名稱為UnitTestDemo.dll.config,將其填入Configuration File Name後面的文字框中,然後我們再次點選[Run]按鈕就會看到測試通過。
總結
作為xUnit體系中的一員,NUnit確實給.Net開發人員進行單元測試帶來了不少方便,在早期我們一直都是使用NUnit進行單元測試的。但是也存在著一些不足之處,比如:1.在xUnit體系中的JUnit是在測試每個方法時都是新生成一個例項,而在NUnit中確實一個TestFixture只會生成一個例項,這樣一來如果對要包含單元測試類中的例項資料進行更改會可能會影響到其它的測試方法(像JUnit那樣每次都生成一個例項則不會產生這種情況)。2.早期大多數人以為像JUnit中一樣,[SetUp]、[TearDown]只會在所有測試前、後分別執行一次,實際情況是在每個測試前、後都會執行一次,為了達到JUnit中[SetUp]、[TearDown]這樣的效果,只能新增TestFixtureSetUp、TestFixtureTearDown屬性。除此之外,還存在一些缺點和不足。
所以本篇只是簡單介紹了NUnit的一些用法,雖然NUnit提供了相當多的斷言及Attribute,但實際用到的並不多,在這裡介紹它是為介紹另一個.NET單元測試工具作鋪墊。