[小北De編程手記] : Lesson 05 玩轉 xUnit.Net 之 從Assert談UT框架實踐 [小北De編程手記] : Selenium For C# 教程

  這一篇,本文會介紹一下基本的斷言概念,但重點會放在企業級單元測試的相關功能上面。下面來跟你們分享一下xUnit.Net的斷言,主要涉及到如下內容:html

  • 關於斷言的概念
  • xUnit.Net經常使用的斷言
  • 關於單元測試實踐的討論
  • xUnit.Net比較器:IEqualityComparer接口
  • 重構Demo:淺談UT框架實踐
  • 擴展實現 : 集合比較
  • 異步處理
  • 結合.Net平臺能力:類型擴展

(一)關於斷言的概念

提到斷言,我想先說說概念上的東西。其實,斷言不是單元測試纔有的東西。先看一段斷言的概念描述:git

  斷言表示爲一些布爾表達式,程序員相信在程序中的某個特定點該表達式值爲真,能夠在任什麼時候候啓用和禁用斷言驗證,所以能夠在測試時啓用斷言而在部署時禁用斷言。一樣,程序投入運行後,最終用戶在遇到問題時能夠從新啓用斷言。使用斷言能夠建立更穩定、品質更好且不易於出錯的代碼。當須要在一個值爲FALSE時中斷當前操做的話,可使用斷言(單元測試必須使用斷言)。程序員

  以上是我認爲比較靠譜的一段關於斷言的定義。換言之,斷言實際上是用來判斷程序是否運行正常(好比:檢查數據完整性,邏輯是否正確等等)的功能,它是能夠打開和關閉的。這一點在.NET和Java等常見的開發平臺下都有相關的支持。而這一功能被普遍的運用在單元測試領域,開篇先強調一下這一點,以避免錯誤的引導了閱讀本文的小夥伴。github

(二)xUnit.Net經常使用的斷言

  談到具體的斷言部分,相信只要是稍微瞭解過單元測試的小夥伴都不會感到陌生,這裏我就再也不贅述了。簡單的列出xUnit.Net經常使用的一些斷言列表:express

  • Equal / NotEqual : 驗證兩個對象值相等
  • Same / NotSame : 驗證兩個對象引用是否相同
  • Contains / DoesNotContain : 驗證對象是否包含(字符串\可枚舉的集合)
  • InRange / NotInRange : 驗證對象是否在某個範圍內
  • Empty / NotEmpty : 驗證對象是否爲空
  • IsType / IsNotType : 驗證對象是否爲某個類型
  • Null / NotNull : 驗證對象是否爲空
  • True / False : 驗證表達式結果爲True/False
  • IsAssignableFrom : 驗證類型是否能夠轉化爲某個類型
  • IsType / IsNotType : 驗證對象是不是某個類型
  • Throws / ThrowsAsync : 驗證操做是否拋出異常

  須要說明的是,每種方法xUnit.Net都提供了多種重載(包括泛型重載)。下面是官網上提供的關於xUnit.Net與其餘單元測試框架對斷言支持的比較。編程

(三)關於單元測試實踐的討論

  若是隻是寫寫Demo,前面講到的東西已經足夠了。但若是你真的想把項目的單元測試作好(搭建一個企業級的單元測試框架),我下面要講的東西也許是更重要的。企業級的單元測試常常會被你們冠以相似這樣的名頭:「單元測試須要作,可是... ... 項目時間緊、代碼不可測試、Mock數據以後覆蓋率底下......(此處省略N多緣由)」。單元測試的好處毋庸置疑,可是實踐起來卻屢屢受挫。我想主要的緣由有如下兩點:設計模式

  • 沒有好的管理機制的要求:項目每每是要求功能的產出。
  • 內部框架支持很差。

  第一點這裏就很少說了,這個系列是技術主導的系列(關於管理相關的實踐,我會在敏捷相關的系列中跟你們分享)。那什麼是內部框架的支持呢?也就是團隊內部對單元測試框架恰當的擴展和封裝。請注意,這裏我用了恰當兩個字,這纔是重點呦~~。目前,封裝,架構,設計模式等等詞語滿天飛。我常常看見一些初出茅廬的小夥伴大談模式和框架,動不動就要重構系統(結果,你懂的~~~)。在我看來,全部項目都會有本身的框架(即便你沒有作任何的設計)。這裏,須要提出一個問題:咱們的設計是不是恰當的?其實,這是一個很難給出準確判斷的事情,但具體的判斷方式卻很簡單。就是看一下框架有沒有達成最初的目的(對如何開始架構設計有興趣的小夥伴能夠去讀讀《恰如其分的軟件架構》,也後有機會會跟你們分享讀後感... ...)。好比,咱們設計框架是爲了提升生產率。那麼咱們就應當對複雜的技術細節進行封裝,爲使用框架的人提供簡單、易用、學習成本低的接口。若是設計框架是爲了不安全性問題,那麼最後咱們就須要考量整個框架在實際的使用過程當中的對安全性的提高程度... ... 很遺憾,咱們如今遇到的不少框架是爲了設計框架而設計的框架。結果和咱們的預期南轅北轍。安全

  吐槽到此結束,本文也沒有打算來說一個單元測試框架如何設計。如今,咱們來看看就單元測試的框架設計而言。咱們應當作那些設計和恰當的封裝呢?固然,沒有銀彈能兼顧全部的問題。下面要提到的主要是結合本文提到的知識點作一些相關擴展。架構

(四)xUnit.Net比較器:IEqualityComparer接口

   仔細觀察一下Assert所提供的方法定義,你會發現不少須要比較的操做都提供了一個接收IEqualityComparer對象的實現。這一小節,咱們就來看看如何使用這個功能。顧名思義IEqualityComparer接口用於兩個對象的比較。當咱們須要自定義一些比較邏輯時,這個功能應當是首選。先看一下IEqualityComparer的定義:框架

1 namespace System.Collections.Generic
2 {
3     public interface IEqualityComparer<in T>
4     {
5         bool Equals(T x, T y);
6         int GetHashCode(T obj);
7     }
8 }

  能夠看到,該接口接收一個用於比較的類型,而且定義了兩個方法:

  • Equals : 提供自定義的比較邏輯。
  • GetHashCode : 提供對象HasCode生成邏輯。

  在xUnit.Net執行斷言判斷的時,若是使用了自定義的比較邏輯,就會使用Equals判斷是否相等,用GetHashCode來獲取對象的標識(這個不是每次都會用到,有興趣能夠看看xUNit.Net的源碼)。下面的Code是來自xUnit.Net官網的列子:

 1         class DateComparer : IEqualityComparer<DateTime>
 2         {
 3             public bool Equals(DateTime x, DateTime y)
 4             {
 5                 return x.Date == y.Date;
 6             }
 7 
 8             public int GetHashCode(DateTime obj)
 9             {
10                 return obj.GetHashCode();
11             }
12         }
13 
14         [Fact(DisplayName = "Assert.DateComparer.Demo01")]
15         public void Assert_DateComparer_Demo01()
16         {
17             var firstTime = DateTime.Now.Date;
18             var later = firstTime.AddMinutes(90);
19 
20             Assert.NotEqual(firstTime, later);
21             Assert.Equal(firstTime, later, new DateComparer());
22         }

  這裏只是一個Demo,更多的使用場景是咱們能夠用這樣的方式爲自定義的類提供比較邏輯。上面的代碼簡單的實現了針對日期的比較邏輯,步驟以下:

  1. 建立DateComparer類,實現IEqualityComparer接口定義的方法。
  2. 在Assert.Equal 中使用對應的方法並傳入相應的比較類(其中定義了比較邏輯)。

(五)重構Demo:淺談UT框架實踐

  說到這裏,若是你只是想了解一下xUnit.Net的使用。那麼,你能夠跳過這一部分,從下一個小節開始看了。我準備從設計比較函數的角度來談談單元測試框架的設計(也一樣適用於不少的開發框架設計)。

  在實際的應用中,你可使用Demo中的操做(不少公司也確實是這麼作的)。隨着項目的演進(一段時間之後)你會發現代碼中處處散落着實現了IEqualityComparer的對象,這樣的維護成本可想而知。建議的作法是對系統中IEqualityComparer類型統一封裝,同時使用單件模式他們的構造進行控制。

首先,咱們來使用單件模式重構剛剛的DateComparer類,如Code標黑的部分所示,這裏屏蔽了DateComparer的構造函數,並實現了單件模式的調用:

 1     class DateComparer : IEqualityComparer<DateTime>
 2     {
 3         private DateComparer() { }  4 
 5         private static DateComparer _instance;  6         public static DateComparer Instance  7  {  8             get
 9  { 10                 if (_instance == null) 11  { 12                     _instance = new DateComparer(); 13  } 14                 return _instance; 15  } 16  } 17 
18         public bool Equals(DateTime x, DateTime y)
19         {
20             return x.Date == y.Date;
21         }
22 
23         public int GetHashCode(DateTime obj)
24         {
25             return obj.GetHashCode();
26         }
27     }

其次,建立一個工廠類統一的控制全部單件比較類的建立邏輯:

1     class SingletonFactory
2     {
3         public static DateComparer CreateDateComparer()
4         {
5             return DateComparer.Instance;
6         }
7         //Other Comparer ... ...
8     }

如今,實際的測試代碼會變成下面的樣子:

 1     public class SingletonFactory_Demo
 2     {
 3         [Fact(DisplayName = "Assert.Singleton.DateComparer.Demo01")]
 4         public void Assert_Singleton_DateComparer_Demo01()
 5         {
 6             var firstTime = DateTime.Now.Date;
 7             var later = firstTime.AddMinutes(90);
 8 
 9             Assert.NotEqual(firstTime, later);
10             Assert.Equal(firstTime, later, SingletonFactory.CreateDateComparer());
11         }
12     }

  其實,調用自己的代碼量並無減小(反而多了),那麼咱們爲何要這樣實現呢?回到以前關於單元測試實踐的討論中所提到的。對於單元測試框架的設計咱們的目的是什麼? 這裏我列出來幾個:

  • 提升開發效率(下降框架使用者的學習成本)。
  • 易於維護和管理。
  • 下降Test Case運行的時間成本

  關於使用者的成本,以前的作法須要使用者明白如何構建IEqualityComparer接口,並定義比較方法。然後一種方法,IEqualityComparer的實現是由框架開發人員或者熟悉xUnit.Net的資深程序員來作的。也就是說,下降了框架使用者的學習成本。也許有人會反駁,這個邏輯很簡單不須要封裝。若是咱們須要比較的對象描述是兩份很複雜的財務報表的對象呢?這樣的比較邏輯是否是須要具備相關的業務知識以及對IEqualityComparer接口(雖然接口很簡單)的瞭解呢?這個樣封裝使得複雜的邏輯被分離開來。

  關於可維護性要從兩個方面來講。第一,實際的項目中全部針對IEqualityComparer的實現都是統一維護的,不管是建立者仍是後來的維護人員都能輕易的找到系統中已有的實現。第二,因爲作了一些拆分。可讓更熟悉比較邏輯(複雜對象)的專家來完成框架的代碼。而開發人員能夠專心的編寫測試邏輯,而不是關注對象比較。

  關於下降Test Case運行的時間成本。試想一下,若是比較對象的是很耗時或者資源開銷的操做(例如須要調用外部的服務....),使用單例模式是否是就大大減低了這方面的成本。

  與此同時,咱們會提出一個架構層面的約定:「不要直接使用實現了IEqualityComparer的比較對象,若是當前的測試框架沒有提供你想要的功能,請按框架的實現方式提交你的Code」。 相信你們很容易理解這個約定的緣由,可是若是它是在本文的一開始就提出的,你也許會以爲很不可理解吧~~~~ 那麼,什麼是框架設計?

  個人理解: 框架設計 = 恰當的約束 + Code;

  Code就是框架代碼的具體實現,而約束偏偏是更加劇要的一環。若是一個框架沒有靠譜的約束列表,最後的實現和架構師起初的設想必定是南轅北轍的。

  固然,用Assert來談UT的框架設計,貌似有些管中窺豹的味道。這裏只是想用例子來分享一下本人對框架設計的一些認識。又扯遠了,仍是回到xUnit.Net的功能上面吧。

(六)擴展實現 : 集合比較Demo

  言歸正傳,前面咱們已經向你們展現瞭如何在類型級別擴展斷言的比較能力(即IEqualityComparer<T>接口),這裏咱們來實現一個邏輯稍複雜也更加實用一些的比較類。先看以一下場景 : 咱們要比較兩個集合的內容是否一致(與數據數序無關)。若是理解了以前的例子,實現這個功能應該很容易:

  首先,定義實現了IEqualityComparer接口的比較類,也以前不一樣的是:這裏泛型的類型指定爲可枚舉的集合(IEnumerable<T>)。比較方法中,排序對比結果。

 1         class CollectionEquivalenceComparer<T> : IEqualityComparer<IEnumerable<T>> 
 2             where T : IEquatable<T>
 3         {
 4             public bool Equals(IEnumerable<T> x, IEnumerable<T> y)
 5             {
 6                 List<T> leftList = new List<T>(x);
 7                 List<T> rightList = new List<T>(y);
 8                 leftList.Sort();
 9                 rightList.Sort();
10 
11                 IEnumerator<T> enumeratorX = leftList.GetEnumerator();
12                 IEnumerator<T> enumeratorY = rightList.GetEnumerator();
13 
14                 while (true)
15                 {
16                     bool hasNextX = enumeratorX.MoveNext();
17                     bool hasNextY = enumeratorY.MoveNext();
18 
19                     if (!hasNextX || !hasNextY)
20                         return (hasNextX == hasNextY);
21 
22                     if (!enumeratorX.Current.Equals(enumeratorY.Current))
23                         return false;
24                 }
25             }
26 
27             public int GetHashCode(IEnumerable<T> obj)
28             {
29                 throw new NotImplementedException();
30             }
31         }

  而後,咱們看一下Test Case:

 1         [Fact]
 2         public void DuplicatedItemInOneListOnly()
 3         {
 4             List<int> left = new List<int>(new int[] { 4, 16, 12, 27, 4 });
 5             List<int> right = new List<int>(new int[] { 4, 12, 16, 27 });
 6 
 7             Assert.NotEqual(left, right, new CollectionEquivalenceComparer<int>());
 8         }
 9 
10         [Fact]
11         public void DuplicatedItemInBothLists()
12         {
13             List<int> left = new List<int>(new int[] { 4, 16, 12, 27, 4 });
14             List<int> right = new List<int>(new int[] { 4, 12, 16, 4, 27 });
15 
16             Assert.Equal(left, right, new CollectionEquivalenceComparer<int>());
17         }

  例子很簡單,但倒是不少單元測試會常用的功能。so ... ... 列出來給你們,順便鞏固一下IEqualityComparer的使用。

(七)異步處理

  實際的單元測試中,一些測試方法須要經過異步的方式調用。xUnit.Net很好的結合了C#所提供的異步操做能力。1.9以前的xUnit.Net使用了Task的方式來實現異步操做,這裏就不介紹了(已經是過去時~~~)。1.9以後的xUnit.Net版本結合C#中 async / await 提供的能力。很是簡單的實現了針對異步方法的測試需求,先看一個Demo:

 1     public class Assert_Async
 2     {
 3         [Fact]
 4         public async void CodeThrowsAsync()
 5         {
 6             Func<Task> testCode = () => Task.Factory.StartNew(ThrowingMethod);
 7 
 8             var ex = await Assert.ThrowsAsync<NotImplementedException>(testCode);
 9 
10             Assert.IsType<NotImplementedException>(ex);
11         }
12 
13         [Fact]
14         public async void RecordAsync()
15         {
16             Func<Task> testCode = () => Task.Factory.StartNew(ThrowingMethod);
17 
18             var ex = await Record.ExceptionAsync(testCode);
19 
20             Assert.IsType<NotImplementedException>(ex);
21         }
22 
23         void ThrowingMethod()
24         {
25             throw new NotImplementedException();
26         }
27     }

  如上面的Code所示,使用xUnit.Net編寫異步處理相關的Unit Test,通常有如下幾個步驟:

  1. Test Case 方法標記爲 : async
  2. 定義待測試的方法
  3. 使用Assert.ThrowsAsync或者Record.ExceptionAsync來執行線程操做
  4. 判斷結果

(八)結合.Net平臺能力:類型擴展

  xUnit.Net的一個特色之一,就是充分的發揮了C#語言和.Net平臺自己的能力。從異步的處理就可見一斑,這一部分的最後我打算跟分享一下如何使用靜態擴展方法來加強類型系統自己對斷言的支持。看一下官網提供的Demo:

 1 namespace Xunit.Extensions.AssertExtensions
 2 {
 3     /// <summary>
 4     /// Extensions which provide assertions to classes derived from <see cref="Boolean"/>.
 5     /// </summary>
 6     public static class BooleanAssertionExtensions
 7     {
 8         /// <summary>
 9         /// Verifies that the condition is false.
10         /// </summary>
11         /// <param name="condition">The condition to be tested</param>
12         /// <exception cref="FalseException">Thrown if the condition is not false</exception>
13         public static void ShouldBeFalse(this bool condition)
14         {
15             Assert.False(condition);
16         }
17 
18         /// <summary>
19         /// Verifies that the condition is false.
20         /// </summary>
21         /// <param name="condition">The condition to be tested</param>
22         /// <param name="userMessage">The message to show when the condition is not false</param>
23         /// <exception cref="FalseException">Thrown if the condition is not false</exception>
24         public static void ShouldBeFalse(this bool condition,
25                                          string userMessage)
26         {
27             Assert.False(condition, userMessage);
28         }
29 
30         /// <summary>
31         /// Verifies that an expression is true.
32         /// </summary>
33         /// <param name="condition">The condition to be inspected</param>
34         /// <exception cref="TrueException">Thrown when the condition is false</exception>
35         public static void ShouldBeTrue(this bool condition)
36         {
37             Assert.True(condition);
38         }
39 
40         /// <summary>
41         /// Verifies that an expression is true.
42         /// </summary>
43         /// <param name="condition">The condition to be inspected</param>
44         /// <param name="userMessage">The message to be shown when the condition is false</param>
45         /// <exception cref="TrueException">Thrown when the condition is false</exception>
46         public static void ShouldBeTrue(this bool condition,
47                                         string userMessage)
48         {
49             Assert.True(condition, userMessage);
50         }
51     }
52 }

  這裏利用了C#的靜態擴展方法對類型bool 進行了擴展(固然,你也能夠擴展任何一個已有的類型),內部使用 Assert 作了一些常規的斷言判斷。如今Unit Test Case的調用代碼就變成了以下所示:

 

 1         [Fact]
 2         public void ShouldBeTrue()
 3         {
 4             Boolean val = true;
 5 
 6             val.ShouldBeTrue(); 
 7         }
 8 
 9         [Fact]
10         public void ShouldBeFalse()
11         {
12             Boolean val = false;
13 
14             val.ShouldBeFalse();
15         }
16 
17         [Fact]
18         public void ShouldBeTrueWithMessage()
19         {
20             Boolean val = false;
21 
22             Exception exception = Record.Exception(() => val.ShouldBeTrue("should be true"));
23 
24             Assert.StartsWith("should be true", exception.Message); 
25         }

 

  固然,對於這種用法本人仍是持保留意見的。畢竟只是Assert的簡單封裝,更像是語法糖。但貌似不少團隊有這樣的開發風格,仁者見仁啦~~~。

總結:

  本文主要介紹了xUnit.Net的斷言相關的使用,擴展。也簡單的談到了UT(Unit Test)框架的設計。這一篇文章吐了不少槽,回顧一下:

  • 關於斷言的概念
  • xUnit.Net經常使用的斷言
  • 關於單元測試實踐的討論
  • xUnit.Net比較器:IEqualityComparer接口
  • 重構Demo:淺談UT框架實踐
  • 擴展實現 : 集合比較
  • 異步處理
  • 結合.Net平臺能力:類型擴展

小北De系列文章:

  《[小北De編程手記] : Selenium For C# 教程

  《[小北De編程手記]:C# 進化史》(未完成)

  《[小北De編程手記]:玩轉 xUnit.Net》(未完成)

Demo地址:https://github.com/DemoCnblogs/xUnit.Net

若是您認爲這篇文章還不錯或者有所收穫,能夠點擊右下角的 【推薦】按鈕,由於你的支持是我繼續寫做,分享的最大動力!
做者:小北@North
來源:http://www.cnblogs.com/NorthAlan
聲明:本博客原創文字只表明本人工做中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關係。非商業,未受權,貼子請以現狀保留,轉載時必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。
相關文章
相關標籤/搜索