.net持續集成測試篇之Nunit 測試配置
- 2019 年 10 月 3 日
- 筆記
在開始之前我們先看一個陷阱
用到的Person類如下
public class Person:IPerson { public string Name { get; set; } public int Age { get; set; } public DateTime BirthDay { get; set; } /// <summary> /// 判斷Name是否包含字母B /// </summary> /// <returns></returns> public bool WhetherNameContainsB() { if (this.Name == null) throw new ArgumentNullException("參數不能為null"); if (this.Name.Contains("B")) return true; return false; } }
這個類以前也用過,有三個屬性和一個方法,其中方法用於判斷Name字段是否包含大寫字母B,如果包含返回true,不包含返回false,如果Name為null則拋出異常
測試類如下
[TestFixture] public class FirstUnitTest { private Person psn; public FirstUnitTest() { psn = new Person(); } [Test] [Order(1)] public void SetPersonName() { psn.Name = "sto"; Assert.IsNotEmpty(psn.Name); } [Test] [Order(2)] public void DemoTest() { Assert.Throws<ArgumentNullException>(() => psn.WhetherNameContainsB()); } }
第一個測試給Name賦值,然後斷言用戶名不為空,這顯然應該是通過的
第二個測試用於斷言調用WhetherNameContainsB時會拋異常,由於這裡Name並沒有賦值,所以會拋出異常,這裡也應該能返回成功.
然而運行以上代碼第二個測試返回的是失敗!這是因為Nunit在運行測試類的時候會調用所有的測試方法,由於我們顯式指定的運行順序(使用order註解)
則第一個方法先於第二個方法前執行,由於第一個方法把Name設置為"sto",因此這時候全局psn的Name字段便有值了.所以第二個方法再調用psn的WhetherNameContainsB方法時,是不會拋出異常的(方法的邏輯是只有Name有值便不會拋出異常).
如果不指定運行順序,則第二個方法運行的結果是不確定的,如果它先於第一個方法執行,則就會返回成功,如果晚於第一個方法則返回失敗.
我們前面說到,單元測試的結果應該是穩定的,然而這裡卻是不確定的,因此我們要重新設計.
當然其實解決這個問題很簡單,只要把對全局的變量移動到方法裏面就行了,這樣每個方法的狀態就不會被外部改變了.
改造後的測試類如下
[TestFixture] public class FirstUnitTest { public FirstUnitTest() { } [Test] [Order(1)] public void SetPersonName() { Person psn = new Person(); psn.Name = "sto"; Assert.IsNotEmpty(psn.Name); } [Test] [Order(2)] public void DemoTest() { Person psn = new Person(); Assert.Throws<ArgumentNullException>(() => psn.WhetherNameContainsB()); } }
我們再運行,便都能通過了.
然而這樣設計有一個問題,第一如果多個測試方法都要用到這個對象,則需要複製很多,第二如果多個方法之間共用的代碼非常多,那麼每個方法里都要複製很多代碼,我們前面說過單元測試里的代碼應力求簡潔明了,並且複製同樣的代碼不利於維護.下面我們介紹Nunit里的Setup
Setup注釋
在單元測試類中如果把一個方法加上setup註解,則這個方法會先於其它未標的方法執行,並且每個方法執行之前都會執行它
,如果在setup註解的方法內初始化對象,則每個方法運行之前都會運行這個被註解的方法,則每次變量都重新初始化,不會再有數據被共享造成的各種問題了.我們用setup改造後的測試類如下
[TestFixture] public class FirstUnitTest { private Person psn; public FirstUnitTest() { } [SetUp] public void Setup() { psn = new Person(); } [Test] [Order(1)] public void SetPersonName() { psn.Name = "sto"; Assert.IsNotEmpty(psn.Name); } [Test] [Order(2)] public void DemoTest() { Assert.Throws<ArgumentNullException>(() => psn.WhetherNameContainsB()); } }
我們在標識為Setup的方法里初始化Person,這樣測試就能通過了
被Setup註解的方法名可任意取,只要符合命名規範即可
Nunit並不限制一個測試類中有多個Setup方法,但是強烈不建議這麼做.
OneTimeSetup注釋
OneTimeSetup也是在所有的測試方法運行之前運行,不同的是它並不像SetUp一樣每個測試方法運行之前都會運行,而是在所有測試方法運行之前之運行一次.它適用這樣場景:比如說我們程序里的數據訪問封閉類,這個類裏面一般都是訪問數據庫的各種方法和一些私有的變量像連接字符串之類的,數據訪問方法里只會去讀取這些字段而不去修改它.最為重要的是每個測試方法運行之前都去實體化一個這樣的類會很耗費資源.像這種類型便可以放在OneTimeSetup方法里,在類創建的時候運行一次.
這個方法功能很像構造函數,它能做的工作一般構造函數也能做.
Teardown
Teardown和Setup用法一樣,只是它是在測試方法運行之後才運行,如果我們的測試方法里有需要釋放的對象可以在這個方法里釋放.
OneTimeTearDown
它是在所有的方法都運行完之後才運行一次,功能上相當於析構函數,用於在測試類所有方法都執行完以後釋放掉類中使用的資源.
前面部分我們講了如何在所在單元測試運行之前以及在每一個單元測試之前如何運行一個特定的方法.下面講解如何在程序集運行之前和運行之後運行某一指定方法.
可能會有人懷疑這樣做的意義,的確,大部分時候我們可能不需要在程序集運行之前或者之後運行某一方法,但是特定的情況下這樣做確實會給測試帶來很大幫助.比如以下場景
- 我們想要統計一下所有測試方法的運行時間,這時候我們可以在程序集之前啟動StopWatch並在所有方法運行完之後獲得運行時間,並寫入日誌.當然這樣做可能顯得有點傻.
-
在Web項目中可能會大量使用ConfigurationManager.AppSetting[xxx]來獲取web項目配置,這樣做給測試帶來難題
由於單元測試的運行環境很多時候並非在程序的輸出目錄,因此web項目使用到AppSetting配置的方法在web環境運行正常,但是在單元測試環境得到的值都是Null,這將會導致測試時大量業務覆蓋不到.
在測試的時候我們很難通過傳參來改變這個值,因為在程序中往往都是獲取AppSetting里的值,而不是設置,因此它往往不包含在方法的參數里.也就沒法通過傳參來修改它.
我們如果在Setup里給AppSetting賦值,比如ConfigurationManager.AppSettings["user"] = "sto";這樣在運行的時候我們便可以獲取到這個值了,但是AppSetting是全局的,可能程序中很多方法都用到了它,我們在每個測試方法里都寫個Setup方法給它複製顯然非常boring.
這時候我們可以在程序集運行之前運行一個方法,在這個方法里給AppSetting賦值,這樣測試方法運行的時候使用到AppSetting的地方就可以獲取到值了.
要做到這一點,我們需要新建一個類,並把類上加上SetUpFixture註解.然後方法上加上OneTimeSetUp和OneTimeTeardown註解.這樣Nunit就會在程序集加載的時候掃描到這個類,然後對它處理.
我們看一下示例代碼
[SetUpFixture] public class AssemblySetup { [OneTimeSetUp] public void RunBeforeEveryMethod() { ConfigurationManager.AppSettings["user"] = "sto"; ConfigurationManager.AppSettings["age"] = "32"; } }
我們新建這個類以後
RunBeforeEveryMethod
便會在程序集中所有代碼運行之前運行了
我們看運行結果
我們可以看到,在測試類中隨便找一個方法裏面去獲取值,都可以獲取到了.
前面我們講解了如何在方法運行前後,在測試類的所有方法運行前後以及如何在程序集,下面我們講一下如何自定義一個方法在測試方法運行之前/之後運行.
自定義方法的優勢在於如果每個測試類的setup里運行的代碼基本相同,只是稍微有一點差異,這樣就會導致代碼重複的問題.比如我們要在方法運行之前和之後記錄一些日誌,這樣我們就可以自定義一個方法實現在測試方法運行前後運行這個自定義方法,減少代碼重複.
要實現自定義運行方法,我們要繼承TestactionAttribute
示例代碼如下
public class MyTestAction:TestActionAttribute { public override void BeforeTest(ITest test) { Console.WriteLine("★★★★★★★★★★" + test.FullName); } }
我們用Console.WriteLine模擬.
Itest對象由Nunit在運行時注入.
然後我們要在運行這個自定義方法的類上加上MyTestAction
註解即可.
自定義運行方法非常強大,還可以提供參數,這樣會在大幅度減少相似代碼的重複,提高可維護性,大家要以後的測試中慢慢體會.