在.NET開發中的單元測試工具之(1)——NUnit

 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)的引用就夠了。
  這裏周公採用的示例代碼如下:
 
  1. using System; 
  2. using System.Collections.Generic; 
  3. using System.Linq; 
  4. using System.Text; 
  5. using NUnit.Framework; 
  6.  
  7. namespace UnitTestDemo 
  8.     [TestFixture] 
  9.     public class NUnitTestDemo 
  10.     { 
  11.         private IList<int> intList = new List<int>(); 
  12.  
  13.         [SetUp] 
  14.         [Category("NA")] 
  15.         public void BeforeTest() 
  16.         { Console.WriteLine("BeforeTest"); } 
  17.  
  18.         [TestFixtureSetUp] 
  19.         [Category("NA")] 
  20.         public void BeforeAllTests() 
  21.         { Console.WriteLine("BeforeAllTests"); } 
  22.  
  23.         [TearDown] 
  24.         [Category("NA")] 
  25.         public void AfterTest() 
  26.         { Console.WriteLine("AfterTest"); } 
  27.  
  28.         [TestFixtureTearDown] 
  29.         [Category("NA")] 
  30.         public void AfterAllTests() 
  31.         { Console.WriteLine("AfterAllTests"); } 
  32.  
  33.         [Test] 
  34.         [Category("NA")] 
  35.         public void Test1() 
  36.         { Console.WriteLine("Test1"); } 
  37.  
  38.         [Test] 
  39.         [Category("NA")] 
  40.         public void Test2() 
  41.         { Console.WriteLine("Test2"); } 
  42.  
  43.         [Test] 
  44.         public void TestFloat() 
  45.         { 
  46.             float value = 0.9999999999999999999999999999f; 
  47.             //value = 0.9999999999999999999999999999; 
  48.             Console.WriteLine("float value:" + value); 
  49.             Assert.AreEqual(value, 1f); 
  50.             Console.WriteLine("TestFloat");  
  51.         } 
  52.  
  53.         [Test] 
  54.         public void TestDouble() 
  55.         { 
  56.             double value = 0.9999999999999999999999999999d; 
  57.             Console.WriteLine("double value:" + value);  
  58.             Assert.AreEqual(value, 1d); 
  59.             Console.WriteLine("Test2");  
  60.         } 
  61.  
  62.         [Test] 
  63.         public void TestDecimal() 
  64.         { 
  65.             decimal value = 0.9999999999999999999999999999M; 
  66.             Console.WriteLine("decimal value:" + value);  
  67.             Assert.AreEqual(value, 1M); 
  68.             Console.WriteLine("Test2");  
  69.         } 
  70.  
  71.         [Test,Repeat(3)] 
  72.         public void TestIntList2() 
  73.         { 
  74.             Assert.AreEqual(0, intList.Count); 
  75.         } 
  76.  
  77.         [Test] 
  78.         public void TestIntList1() 
  79.         { 
  80.             intList.Add(1); 
  81.             Assert.AreEqual(1, intList.Count); 
  82.         } 
  83.  
  84.         [TestCase(12, 3, 4)] 
  85.         [TestCase(12, 2, 6)] 
  86.         [TestCase(12, 4, 3)] 
  87.         public void DivideTest(int n, int d, int q) 
  88.         { 
  89.             Assert.AreEqual(q, n / d); 
  90.         } 
  91.  
  92.         [Test, Combinatorial,Description("This is used for show Combinatorial")] 
  93.         public void MyTest( 
  94.         [Values(1, 2, 3)] int x, 
  95.         [Values("A""B")] string s) 
  96.         { 
  97.             string value = x + s; 
  98.             Assert.Greater(2, value.Length); 
  99.         } 
  100.  
  101.         [Test] 
  102.         public void TestDemo1( 
  103.         [Values(1, 2, 3)] int x, 
  104.         [Random(-10,10,2)] int y) 
  105.         { 
  106.             Assert.Greater(x + y, 0); 
  107.         } 
  108.  
  109.         [Test] 
  110.         public void TestDemo2( 
  111.         [Range(0, 11, 4)] int x) 
  112.         { 
  113.             Assert.AreEqual(x%3,0); 
  114.         } 
  115.     } 
  編譯項目生成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文件中保存的數據進行測試,我們在剛纔的代碼基礎上編寫了三個測試用例,代碼如下:
  1. [Test] 
  2. public void Test0_51CTOBlog() 
  3.     StringAssert.AreEqualIgnoringCase(ConfigurationManager.AppSettings["51ctoBlog"], "http://zhoufoxcn.blog.51cto.com"); 
  4.  
  5. [Test] 
  6. public void Test0_CSDNBlog() 
  7.     StringAssert.AreEqualIgnoringCase(ConfigurationManager.AppSettings["CSDNBlog"], "http://blog.csdn.net/zhoufoxcn"); 
  8.  
  9. [Test] 
  10. public void Test0_SinaWeiBo() 
  11.     StringAssert.AreEqualIgnoringCase(ConfigurationManager.AppSettings["SinaWeiBo"], "http://weibo.com/zhoufoxcn"); 
  同時在app.config文件的appSettings節點增加以下數據:
  1. <appSettings> 
  2.     <add key="51ctoBlog" value="http://zhoufoxcn.blog.51cto.com"/> 
  3.     <add key="CSDNBlog" value="http://blog.csdn.net/zhoufoxcn"/> 
  4.     <add key="SinaWeiBo" value="http://weibo.com/zhoufoxcn"/> 
  5. </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單元測試工具作鋪墊。
  周金橋
  2013-01-03

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章