為了支持跨平台,微軟為.net平台提供了.net core test sdk,這樣第三方測試框架諸如Nunit,Xunit等只需要按照sdk提供的api規范進行開發便可以被dotnet cli工具調用,這樣就解決了在持續集成過程中第三方框架依賴於windows平台上的各自runner的問題,使得測試框架開發者不需要花費很大功夫就可以快速遷移到.net core平台,同時封裝了各測試框架的實現細節,對外暴露統一調用接口,大大減少devops開發者的工作量.
作為單元測試基礎知識介紹,這里只介紹常用單元測試框架Nunit和Xunit如何在.net core平台上使用,並介紹由於.net core的變化所引起的需要注意的測試代碼的相應改變,對於如何在Jenkins環境中自動完成測試的相關內容Jenkins基礎知識里面介紹
考慮到實際工作中可能有的項目組已經使用或者嘗試使用xunit,並且.net core對xunit支持較好,諸多開源項目的單元測試也都使用的是Xunit,本章節作為補充對Xunit基礎知識進行講解,以幫助還不太了解這個框架的同學快速入門.
我們知道在.net framework下相要對mvc項目進行集成測試非常困難,一方面.net http管道里有很多黑盒子,開發者對它的實現細節一無所知,二者需要mock的對象太多,工作量巨大.因此很難在持續集成環境中對web項目進行集成測試.開發者或者測試人員大都依賴postman,fiddler,以及瀏覽器的http請求插件來進行集成測試,這樣帶來的一個很大問題這些http請求很難復用,更難以有效的組織管理.即使使用postman這樣強大的http工具如果測試接口過多代碼也會變得一塌糊塗,過一段時間后想要知道哪個方法是測試哪個接口用的就需要通過搜索來導航到指定的測試方法,並且很多時候有於各模塊有相同名稱的方法,往往需要先找到方法所在的area,然后再找到controller然后再找到相應方法...如果出現問題的代碼過多往往把開發者搞的焦頭爛額,苦不堪言.
幸運的是在.net core里很容易模擬一個httpt管道,這一方面使得集成測試在持續集成環境中使用提供了可能,另一方http請求寫成程序里,可以很方便的導航到指定的測試方法,極大提供可維護性.本章節最后會介紹如何搭建一個.net core web項目的selfhost環境以供在單元測試框架中使用.
下面我們將簡要介紹如何在vs中配置xunt環境以及Xunit斷言的基本使用
.net core 中使用Xunit
Xunit是.net平台下的一款單元測試工具,類似Nunit.但是更為輕量,更加專注於單元測試而不像Nunit提供了很多額外的功能
.net core對Xunit支持較好,VisualStudio 2017提供有一個Xunit單元測試模板可以很方便的創建一個Xunit單元測試項目.

如圖,在visual studio里創建項目時,選擇.net core項目,然后從模板里面找到Xunit單元測試項目便可以創建一個Xunit單元測試項目了.
我們打開剛創建的項目右鍵選擇"Nuget包管理",從包管理工具里我們可以看到,實際上這個模板引用了xunit,和xunit.runner.visualstudio這兩個包.這樣,我們也可以自己手動創建一個.net core類型的庫文件,然后引入這兩個包,能達到同樣的效果.
這里建議大家通過模板來創建單元測試項目,因為單元測試框架不同版本可能需要引用不同的包,沒有經驗的同學常常由於引用的包不對導致單元測試項目跑不起來,搞得灰頭土臉,非常郁悶.
我們編寫以下單元測試代碼
public class UnitTest1
{
[Fact]
public void Test1()
{
int intt = 3 + 2;
Assert.True(intt==5);
}
}
通過以上代碼我們看到Xunit就測試斷言和Nunit很類似(這里指Nunit3,早期版本Nunit差異較大,建議大這在工作中也盡量選用Nunit3,而不是1或者2)
這里有一點差異需要指出,Xunit並不需要對單元測試類進行注解(Nunit是需要的,否則無法識別),只需要在需要測試的方法上加上fact注解即可.
單元測試方法的運行也和前面講的Nunit單元測試運行方法相同,這里不再贅述.
常見基本斷言
雖然Xunit和Nunit在斷言上有很多相似的地方,並且有越來越像的趨勢,但是仍然有不少差別,因此這里仍然會對Xunit的斷言功能進行一個全面的列舉,以供大家速查.並且有時候會指出它和Nunit的差別或者指出Nunit中比較難以實現或者技巧性很強的功能如何在Xunit里實現.如果有讀者直接閱讀本章節而沒有了解過Nunit,可以有選擇的略過二者比較的內容.
這里首先指出一個很大的差別.Xunit里並沒有像Nunit里的stringAssertion,FileAssertion和CollectionAssertion,而是所有的斷言都在Assert靜態類里,方法也不是很多,語義也相對更加明確,很適應沒有單元測試基礎的同學快速入門.
下面開始介紹Xunit里的斷言方法
Assert.Null
用於斷言一個對象是否是Null
這個方法有一個相對含義的斷言就是Assert.NotNull,很多其它的方法也有帶Not的斷言,很容易理解.
Assert. Assert.Equal
此方法有很多重載,用於比較兩個字符串,int,decimal或者對象類型是否相等.
注意這里比較兩個對象是否相等時,相當於Object.equals()來比較兩個對象是否相等
這個方法用於比較兩個double類型值是否相等時,可以指定精度.
Assert.StrictEqual
從字面上來看,它用於比較兩個對象是否是嚴格相等,然而它的表現行為和以上Equal方法非常類似,並不是比較兩個對象內存地址是否相等.實際上它是在比較的時候指定一個默認的比較器.這個方法着實非常讓人困惑.
Assert.same
用於比較兩個對象運行是是否指定同一塊內存地址,如果要比較兩個對象是否完全相等,則使用它.
注意,雖然Assert.same接收的是兩個object類型對象,但是不建議用它來比較簡單類型,它其實是用於比較兩個對象是否指向同一內存地址,因此只有比較引用對象才是有意義的.這個方法應該設計成泛型方法才比較好,不知道為什么要這樣設計.
Assert.StartsWith Assert.EndsWith
用於斷言一個字符串是否以特定字符(串)開頭(結尾),並且這兩個方法都有一個重載用於指定是否忽略大小寫
用過Nunit的同學可能知道,Nunit里要實現區分大小寫的StartsWith有點麻煩.
Assert.True Assert.False
用於斷言兩個布爾變量(包括可空)的值是否是真(假).
Assert.All
用於斷言集合里的所有元素是否都滿足通過測試,奇怪的是這個方法接收的是Action
下面舉個用於斷言集合里的元素值是否都大於0的例子來看看如何使用它
[Fact]
public void Test1()
{
int[] intt = {3, 4, 5, 9, 22};
Assert.All(intt, t => Assert.True(t > 0));
}
注意以上寫法,由於不接收Func<T,bool>類型委托,因此以上方法不能想當然的寫為 Assert.All(intt, t => t>0); 這樣將導致編譯錯誤.
Assert.Contains
這個方法有多個重載,功能也非常多,但是語義都非常明確.
用於斷言字符串是否包含指定字符串
相當於字符串里的Contains,並且表現行為類似,也有一個重載支持不區分大小寫
用於斷言集合是否包含指定元素
請看以下代碼
[Fact]
public void Test1()
{
int[] intt = {3, 4, 5, 9};
Assert.Contains(4, intt);
}
以上代碼用於斷言集合intt里是否包含元素4,顯然是包含的
注意,對於包含引用對象的集合判斷是否包含某一元素這一個元素必須和集合中的某一個元素的引用地址一樣.這很多時候並不是我們想要的行為,多數時候引用對象的對應的字段分別相等時我們就認為它相等,更極端的情況是某些情況下兩個對象只有某一個或者少數幾個字段相等時我們也認為相等,這要看具體實際業務.前面講Nunit時對這個問題有過詳細講解.這里Contains方法同樣有一個重載以支持一個
比較器,用於自定義相等性邏輯.
還有一點需要指出,Contains方法只能斷言集合是否包含某
一個元素,而不能斷言是否包含某幾個元素(也即一個集合是否是另一個集合的子集),Nunit里並沒有提供直接方法用於處理這樣的問題.有些同學可能認為Assert.Subset是用來解決這個問題的,然而並不是,Subset只能用於實現了ISet接口的集合,很多時候並不是特別有幫助.
用於斷言集合中是否包含指定類型的元素
這個重載方法語義稍顯不是很明確,它其實相當於linq里的any方法,只要有一個(一些)滿足條件的元素就會返回true
[Fact]
public void Test1()
{
int[] intt = {3, 4, 5, 9};
Assert.Contains(intt, a => a > 3);
}
以上方法用於斷言intt集合中是否包含大於3的元素,顯然是包含的.
Assert.Empty
用於斷言集合是否不包含任何元素,也即集合是否是一個空集合
Assert.Matches
此方法接收兩個參數,第一個表示要匹配的正則規則,第二個表示要測試的字符串,語義類似正則表達式里的IsMatch
Assert.InRange
用於斷言指定元素是否在指定的范圍內
[Fact]
public void Test1()
{
Assert.InRange(20, 3, 20);
}
以上代碼片段斷言20是否在3到20這個范圍內,顯然是在的.
此方法支持一個重載接收一個Icomparer參數用於自定義一個比較器,這樣就可以判斷任意對象是否在某一范圍內,有這方面需求的同學可以研究一下
Assert.Single
用於斷言集合只包含 一個 元素,這個方法有幾個重載.
重載1
用於斷言集合中是僅包含一個元素,這個重載可能大部分時候不是很有用
[Fact]
public void Test1()
{
Assert.Single(new[] {3});
}
重載2 接收一個參數,用於斷言這個元素是否是指定元素
[Fact]
public void Test1()
{
Assert.Single(new[] {3,4,5,9},3);
}
以上代碼斷言集體中只有一個元素是3
此方法相當於對集合執行linq的first方法
重載3 接收一個predict類型委托,用於斷言這個元素是否滿足指定條件
[Fact]
public void Test1()
{
Assert.Single(new[] {3,4,5,9},a=>a>5);
}
以上方法斷言集合中的這個是否只包含一個大於5的元素.
此方法相當於linq里面的single方法的有參重載
Assert.IsAssignableFrom
用於斷言實例對象的類型是否是一個類型的子類(或者本身)
這個方法Nunit里也有,和反射里的
IsAssignableFrom語義相同
Assert.IsType
用於斷言實例對象的類型是否是某一指定類型
和IsAssignableFrom相比,此方法要求實例對象類型必須確切地是某一類型
