1. 為什麼需要單元測試 在我們之前,測試某些功能是否能夠正常運行時,我們都將代碼寫到Main方法中,當我們測試第二個功能時,我們只能選擇將之前的代碼清掉,重新編寫。此時,如果你還想重新測試你之前的功能時,這時你就顯得有些難為情了,因為代碼都被你清掉了。當然你完全可以把代碼寫到一個記事本中進行記錄, ...
在我們之前,測試某些功能是否能夠正常運行時,我們都將代碼寫到Main方法中,當我們測試第二個功能時,我們只能選擇將之前的代碼清掉,重新編寫。此時,如果你還想重新測試你之前的功能時,這時你就顯得有些難為情了,因為代碼都被你清掉了。當然你完全可以把代碼寫到一個記事本中進行記錄,但是這樣總歸沒有那麼方便。當然你也可以重新新建一個項目來測試新的功能,但隨著功能越來越多,重新新建項目使得項目越來越多,變得不易維護,此時你若選擇使用單元測試功能,就可以完美解決你的困擾。
NUnit 提供了單元測試能力,也是目前用的比較流行的單元測試組件。
2. 什麼是NUnit?
官網:
NUnit 是適用於所有 .Net 語言的單元測試框架。最初從
NUnit 項目是
NUnit的成功是通過我們許多貢獻者和
3. NUnit V3 使用
-
通過Nuget 安裝 如下三個包
-
NUnit
-
Microsoft.NET.Test.Sdk
- NUnit3TestAdapter
-
NUnit 包應由每個測試程式集引用,但不應由任何其他測試程式集引用。
NUnit使用自定義特性來標識測試。 所有的NUnit屬性都包含在NUnit中的 框架的名稱空間。 包含測試的每個源文件必須包含該名稱空間的using語句,項目必須引用框架程式集nunit.framework.dll。
4. 特性
Attribute | Usage |
---|---|
指示測試應在特定單元中運行。 | |
提供測試作者的姓名。 | |
為測試指定一個或多個類別。 | |
為提供的值的所有可能組合生成測試用例。 | |
指定應為其運行測試或夾具的區域性。 | |
提供數據 | |
提供數據源 | |
指示測試應使用指定的容差作為浮點數和雙重比較的預設值。 | |
將描述性文本應用於測試、測試修複或程式集。 | |
指示除非顯式運行,否則應跳過測試。 | |
指定夾具的生命周期,允許為每個測試用例構造測試夾具的新實例。在測試用例並行性很重要的情況下很有用。 | |
指示由於某種原因不應運行測試。 | |
指定程式集級別的並行度級別。 | |
指定測試用例成功的最長時間(以毫秒為單位)。 | |
指定測試及其後代不能並行運行。 | |
指定程式集引用 NUnit 框架,但不包含測試。 | |
標識在任何子測試之前要調用一次的方法。 | |
標識在所有子測試之後要調用一次的方法。 | |
指定在包含的夾具或套件中運行裝飾測試的順序。 | |
為提供的所有可能值對生成測試用例。 | |
指示測試和/或其後代是否可以並行運行。 | |
指定應為其運行測試或夾具的平臺。 | |
允許在任何測試用例或夾具上設置命名屬性。 | |
指定生成隨機值作為參數化測試的參數。 | |
指定一系列值作為參數化測試的參數。 | |
指定應多次執行修飾方法。 | |
指示測試方法、類或程式集應在單獨的線程上運行。 | |
如果測試失敗,則導致重新運行測試,最多可達最大次數。 | |
使用按提供的順序排列的值生成測試用例,無需其他組合。 | |
設置測試持續時間內的當前區域性。 | |
設置測試持續時間內的當前 UI 區域性。 | |
指示在每個測試方法之前調用的 TestFixture 方法。 | |
使用命名空間中所有測試夾具的一次性設置或拆卸方法標記類。 | |
標記要求其所有測試在同一線程上運行的夾具。 | |
指示在每個測試方法之後調用的 TestFixture 方法。 | |
標記表示測試的 TestFixture 的方法。 | |
將帶有參數的方法標記為測試,並提供內聯參數。 | |
將帶有參數的方法標記為測試,並提供參數源。 | |
將類標記為測試夾具,並可能提供內聯構造函數參數。 | |
已棄用,同義詞: | |
將類標記為測試夾具,併為構造函數參數提供源。 | |
已棄用,同義詞: | |
指示要測試的類的名稱或類型。 | |
將測試方法標記為理論,這是NUnit中的一種特殊測試。 | |
為測試用例提供超時值(以毫秒為單位)。 | |
為測試方法的參數提供一組內聯值。 | |
為測試方法的參數提供值的源 |
5. TestFixture
此屬性標記包含測試以及(可選)設置或拆解方法的類。
現在,對用作測試夾具的類的大多數限制都已消除。TestFixture類:
-
可以是公共的、受保護的、私有的或內部的。
-
可能是靜態類。
-
可以是泛型的,只要提供了任何類型參數,或者可以從實際參數中推斷出來。
-
可能不是抽象的 - 儘管該屬性可以應用於旨在用作TestFixture基類的抽象類。
-
如果 TestFixtureAttribute 中沒有提供任何參數,則該類必須具有預設構造函數。
-
如果提供了參數,則它們必須與其中一個構造函數匹配。
如果違反了這些限制中的任何一個,則該類不可作為測試運行,並且將顯示為錯誤。
建議構造函數沒有任何副作用,因為 NUnit 可能會在會話過程中多次構造對象。
從 NUnit 2.5 開始,TestFixture 屬性對於非參數化、非通用Fixture是可選的。只要該類包含至少一個標有 Test、TestCase 或 TestCaseSource 屬性的方法,它就會被視為測試夾具。
using NUnit.Framework; namespace MyTest; // [TestFixture] // 2.5 版本以後,可選 public class FirstTest { [Test] public void Test1() { Console.WriteLine("test1,hello"); } }
6. SetUp 設置
此屬性在TestFixture內部使用,以提供在調用每個測試方法之前執行的一組通用函數。
SetUp 方法可以是靜態方法,也可以是實例方法,您可以在夾具中定義多個方法。通常,多個 SetUp 方法僅在繼承層次結構的不同級別定義,如下所述。
如果 SetUp 方法失敗或引發異常,則不會執行測試,並報告失敗或錯誤。
using NUnit.Framework; namespace MyTest; // [TestFixture] // 2.5 版本以後,可選 public class FirstTest { [SetUp] public void Init() { Console.WriteLine("init,初始了一些數據"); } private int a = 10; [OneTimeSetUp] // 只執行一次 public void OneTime() { a++; Console.WriteLine("我只執行一次"); } [Test] public void Test1() { Console.WriteLine("test1,hello"); Console.WriteLine($"a的值是:{a}"); } }
輸出結果:
init,初始了一些數據 test1,hello
繼承
SetUp 屬性繼承自任何基類。因此,如果基類定義了 SetUp 方法,則會在派生類中的每個測試方法之前調用該方法。
您可以在基類中定義一個 SetUp 方法,在派生類中定義另一個方法。NUnit 將在派生類中調用基類 SetUp 方法之前調用基類 SetUp 方法。
警告
如果在派生類中重寫了基類 SetUp 方法,則 NUnit 將不會調用基類 SetUp 方法;NUnit 預計不會使用包括隱藏基方法在內的用法。請註意,每種方法可能都有不同的名稱;只要兩者都存在屬性,每個屬性都將以正確的順序調用。
[SetUp]
筆記
-
儘管可以在同一類中定義多個 SetUp 方法,但您很少應該這樣做。與在繼承層次結構中的單獨類中定義的方法不同,不能保證它們的執行順序。
-
在 .NET 4.0 或更高版本下運行時,如有必要,可以指定非同步方法(c# 中的關鍵字)。
7. 斷言
斷言是任何 xUnit 框架中單元測試的核心,NUnit 也不例外。NUnit 提供一組豐富的斷言作為 Assert 類的靜態方法。
如果斷言失敗,則不返回方法調用並報告錯誤。如果測試包含多個斷言,則不會執行失敗的斷言後面的任何斷言。因此,通常最好嘗試每個測試一個斷言。
每個方法都可以在沒有消息的情況下調用,使用簡單的文本消息或消息和參數。在最後一種情況下,使用提供的文本和參數設置消息的格式。
如果確實存在等效項,則這兩種方法將始終給出相同的結果,因為經典方法的方法都是使用約束在內部實現的。例如。。。
Assert.AreEqual(4, 2+2); Assert.That(2+2, Is.EqualTo(4));
Assert.True()
Assert.True用於斷言布爾參數是否為true Assert.True的重載方法還支持可空布爾參數
Assert.IsTrue
此斷言方法為Assert.True的親兄弟,二者功能一模一樣.
Assert.False
與Assert.True斷言狀態相反,斷言某一參數的結果為false 這裡需要特別說明的是,單元測試應該力求簡單,明瞭,斷言尤其如此.
Assert.Null
用於斷言一個變數是否為null,這裡不再舉例,但是實際中用的卻比較多.
Assert.NotNull
用於斷言一個變數不是null,它和Assert.Null()功能相同,只是斷言的狀態相反.
Nunit里還有其它的首碼有Not的方法,它和不帶Not的方法用法一樣,只是斷言的狀態相反
Assert.Throws 用於斷言特定方法在運行的時候會拋出異常.此方法有泛型版本,非同步版本,這裡僅對非同步版本進行說明 由於示例越來越複製,我們不能只在測試方法內寫一些簡單代碼進行測試了,這裡我們新建一個Person類如下
這個類裡面包含一個WhetherNameContainsB方法,用於判斷實例的Name是否包含字母B, 這個方法裡面有三個邏輯分支,單元測試的時候每一個都要覆蓋到,這裡我們斷言如果name為null則拋出ArgumentNullException 我們編寫如下單元測試方法
運行這個測試,則會返回成功狀態,因為預期的異常出現了.
Assert.IsEmpty
用於斷言欄位串是否為空字元串.
Assert.Positive
用於斷言數字類型(int,long,float,double,decimal等)為正數(大於零的數)
其實很多斷言都可以斷言都可以用Assert.True來完成,比如斷言一個數是否為正數,可以用Assert.True(a>0),這裡由於a只是一個普通變數,使用a>0作為條件主義仍然十分清析,然而到了後面有我們不僅要判斷一個變通變數,還要判斷lambda表達式,如果條件過於複雜,則語義會變得不是特別清析了,使用Assert自帶的靜態方法主義會更加清析,可讀性更高.
Assert.Negative
用於斷言數字類型為負數(小於零,不包括零)
Assert.Zero
用於斷言數字類型為數字零
Assert.NotZero
用於斷言數字類型不是零.
很多時候,Not包含的範圍非常廣,進行單元測試是為了在開發階段找出問題,解決問題,因此斷言的範圍越窄越好,我們不能僅僅讓單元測試通過了事. 比如一個方法返回的結果是數字類型,我們要斷定它是正數?大於某一個數的正數?在一定範圍的正數?是一個具體的正數?而不能簡單的是零,不是零.當然這還要根據業務本身來確實,有些時候範圍可能確實很大,但是一定要註意單元測試原則.
Assert.Greater(OrEqual)
用於斷言數字類型的變數大於(或者等於)某一個值
Assert.Less(OrEqual)
用於斷言數字類型小於(或者等於)某一值
Assert.Contains
用於斷言集合中是否包含某一元素. 比如以下方法,用於斷言字元串數組中是否包含特定字元串
Assert.AreSame
用於斷言兩個對象是否相等
這個靜態方法並沒有提供重載參數用於指定一個比較器來比較引用對象的相等性,需要實現equals和gethashcode方法才能得到預期結果,但在實際中我們往往把比較器放在類外邊,如何在比較引用對象的時候載入一個比較器在後面章節會有介紹,這裡先略過.
8. Nunit測試基礎之複雜斷言
上面一篇我們講解了一些基本斷言,利用這些斷言我們就可以進行單元測試了,然而僅僅使用簡單斷言還是不夠的,如果邏輯複雜度較高,使用簡單的斷言會導致單元測試代碼量增加,最終導致單元測試本身過於複雜和難以維護.需要說明的是這裡所說的複雜斷言仍然在Assert的靜態方法裡面,本身也不是特別複雜,只是比前面講的秒複雜一些,只是如果沒有了這些方法,一些特殊功能實現起來比較費勁基本無法實現.
下麵就介紹一下這些方法.
Assert.Catch
Assert.Catch有泛型和非同步方法,這裡只介紹其泛型方法.很多即使經常使用單元測試功能的人也未必用過這個方法. 其實這個方法和Assert.Throw用法上類似,只是有一點不同的是要測試的方法里的異常可以是catch到的異常的子類,實際開發中,如果我們能確立異常的類型,則最好捕獲具體類型異常,然而不能排除有一些不夠規範的代碼整段代碼被一個try catch包圍,這時候不一定能夠捕獲到想要的特定異常,這時候可以使用Assert.Catch
以上代碼類似上一節中講throw時使用的代碼,只是這裡泛型參數里是Exception而不是具體的異常信息,我們運行這段代碼,依然能夠測試通過. 在單元測試中,期待的狀態越具體越好,然而由於種種原因(比如立項時候沒有對代碼規範做過多要求,開發者水平不高,要測試的代碼是別人寫的,寫單元測試的人對其中邏輯並不是特別清楚等)我們無法做到非常具體,這個時候可以把要獲得的狀態放寬以後,待條件完備了再修改單元測試以進一步收窄狀態.
Assert.Ignore
Assert.Ignore和Ignore註解功能類似,可以在測試的時候忽略一個單元測試.有些情況下我們需要暫時忽略一個測試,比如說要進行測試的內容有一個外部依賴,現在外部依賴暫時不可用,如果我們不忽略的話測試將會失敗,在自動化環境下,失敗將導致無法進行下一步動作,此時我們可以暫時忽略這個測試. 忽略的測試前面有一個 黃色嘆號標誌,警示我們需要註意.
Assert. Fail
我們先看一下麵一段代碼
在這個單元測試本身使用到了try catch,我們知道WhetherNameContainsB方法在Person類的Name沒有提供值的情況下會拋出異常,然而我們的代碼並沒有斷言這個異常存在,此時由於catch代碼塊存在,會把異常吞掉,因此最終我們斷言person的Age為正數的時候將會通過(我們在構造類的時候設置了Age為32) 這顯然不行的,這時候我把們Assert.Fail(e.Message)取消註釋,測試便會變成失敗狀態.
Assert.IsNaN
用於斷言一個Double類型數字是否是NaN
雖然實際業務中我們並不會寫以上代碼,但是如果除數和被除數是通過複雜計算得來的則有可能除數和被除數都是零.
Assert.IsInstanceOf
用於斷言一個對象是否是指定類型的實例,
如上psn是Person類的一個實例,而Person繼承自Object,因此psn也是Object類的實例
Assert.IsAssignableFrom
此方法和以上方法作用相反,它用來斷言指定類型是當前對象類型的子類.(Assert.IsInstanceOf判斷的是當前對象是指定類型的子類) 這個方法語義不是很明確,很容易搞暈,使用的時候需要特別註意
Assert.Warn
用於使一個測試通過,但是出現警示信息.
配套視頻鏈接:
C# 高級編程,.Net6 系列 開發第三階段,學完拿捏你的面試官,.net6 進階學習(已完結)_嗶哩嗶哩_bilibili
海闊平魚躍,天高任我行,給我一片藍天,讓我自由翱翔。