[小北De編程手記] : Lesson 06 玩轉 xUnit.Net 之 定義本身的FactAttribute Lesson 02 玩轉 xUnit.Net 之 基本UnitTest & 數據驅動

  xUnit.Net自己提供了標記測試方法的標籤Fact和Theory。在前面的文章《Lesson 02 玩轉 xUnit.Net 之 基本UnitTest & 數據驅動》中,也對它們作了詳細的介紹。這一篇,來分享一個高級點的主題:如何擴展標籤?仍是老規矩,看一下議題:html

  • 概述
  • 讓xUnit.Net識別你的測試Attribute
  • 定義運行策略:XunitTestCase
  • 與Runner交流:消息總線 - IMessageBus
  • 總結

  這一篇有一些不大容易理解的東東。所以,我是默認讀者已經讀過以前的五篇文章(或者已經充分的瞭解xUnit.Net的基本知識)。另外,最好熟悉面向對象的方法,一些接口編程的實踐。(以前文章的能夠在這裏找到《[小北De編程手記]:玩轉 xUnit.Net》)。固然,要是僅僅達到使用xUnit.Net作作UT,完成基本工做的級別。我想以前的文章所描述的知識點已經足夠了。要是還想進一步瞭解xUnit.Net。那麼,這一篇所講的內容也許是你進階的必經之路。也是本人以爲最好的一個開端... ...git

(一)概述

  在單元測試的實踐中,Fact和Theory已經能知足咱們許多的要求。可是對於一些特殊的狀況,例如:須要屢次運行一個方法的測試用例(10秒鐘內支付接口只能作3次),或者須要開啓多個線程來運行測試用例。這些需求咱們固然能夠經過編碼來完成。但若是能夠用屬性標記的方式來簡單的實現這樣的功能。就會大大下降使用者的編程複雜度,這樣的能力也是在設計一個單元測試框架的時候須要考慮的。xUnit.Net爲咱們提供的優雅的接口,方便咱們對框架自己進行擴展。這一篇,咱們就來介紹如何實現自定義的測試用例運行標籤(相似Fact和Theory)。這一篇的內容略微有點複雜,爲了讓你們能快速的瞭解我要實現什麼樣的功能,先來看一下最終的Test Case:github

 1     public class RetryFactSamples
 2     {
 3         public class CounterFixture
 4         {
 5             public int RunCount;
 6         }
 7 
 8         public class RetryFactSample : IClassFixture<CounterFixture>
 9         {
10             private readonly CounterFixture counter;
11 
12             public RetryFactSample(CounterFixture counter)
13             {
14                 this.counter = counter;
15                 counter.RunCount++;
16             }
17 
18             [RetryFact(MaxRetries = 5)] 19             public void IWillPassTheSecondTime() 20  { 21                 Assert.Equal(2, counter.RunCount); 22  } 23         }
24     }

  能夠看到,用來標記測試用了的屬性標籤再也不是xUnit.Net提供的Fact或者Theory了,取而代之的是自定義的RetryFact標籤。顧名思義,實際的測試過程當中標籤會按照MaxRetries所設置的次數來重複執行被標記的測試用例。自定義運行標籤主要有下面幾個步驟:編程

  • 建立標籤自定義標籤
  • 建立自定義的TestCaseDiscoverer
  • 建立自定義的XunitTestCase子類
  • 重寫消息總線的傳輸邏輯

  該功能也是xUnit.Net官網上提供的示例代碼之一。有興趣的小夥伴能夠去看看,那裏還有不少其餘的Demo。是否是以爲這個功能很不錯呢?接下來我就開始向你們介紹如何實現它吧。app

(二)讓xUnit.Net識別你的測試Attribute

  最開始固然是須要建立一個RetryFact的屬性標籤了,觀察一下Theory的定義。你會發現它是繼承自Fact 並做了一些擴展。所以,咱們自定義的測試標籤頁從這裏開始,代碼以下:框架

1     [XunitTestCaseDiscoverer("Demo.UnitTest.RetryFact.RetryFactDiscoverer", "Demo.UnitTest")]
2     public class RetryFactAttribute : FactAttribute
3     {
4         /// <summary>
5         /// Number of retries allowed for a failed test. If unset (or set less than 1), will
6         /// default to 3 attempts.
7         /// </summary>
8         public int MaxRetries { get; set; }
9     }

  那麼,xUnit.Net如何識別咱們自定義標籤呢?換言之,就是如何知道自定義標籤標記的方法是一個須要Run的測試用例?祕密就在前面代碼中的XunitTestCaseDiscoverer中。咱們須要使用XunitTestCaseDiscoverer標籤爲自定義的屬性類指定一個Discoverer(發現者),並在其中定義返回TestCase的邏輯。代碼以下:less

 1     public class RetryFactDiscoverer : IXunitTestCaseDiscoverer
 2     {
 3         readonly IMessageSink diagnosticMessageSink;
 4 
 5         public RetryFactDiscoverer(IMessageSink diagnosticMessageSink)
 6         {
 7             this.diagnosticMessageSink = diagnosticMessageSink;
 8         }
 9 
10         public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
11         {
12             var maxRetries = factAttribute.GetNamedArgument<int>("MaxRetries");
13             if (maxRetries < 1)
14             {
15                 maxRetries = 3;
16             }
17 
18             yield return new RetryTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, maxRetries);
19         }
20     }

  代碼中添加了對maxRetries初始值修正的邏輯(至少運行3次)。須要說明的是,XunitTestCaseDiscoverer所指定的類應當是實現了IXunitTestCaseDiscoverer接口的(如上面的代碼)。該接口定義了一個xUnit.Net Framework用於發現測試用例的方法Discover。其定義以下:async

 1 namespace Xunit.Sdk
 2 {
 3     // Summary:
 4     //     Interface to be implemented by classes which are used to discover tests cases
 5     //     attached to test methods that are attributed with Xunit.FactAttribute (or
 6     //     a subclass).
 7     public interface IXunitTestCaseDiscoverer
 8     {
 9         // Summary:
10         //     Discover test cases from a test method.
11         //
12         // Parameters:
13         //   discoveryOptions:
14         //     The discovery options to be used.
15         //
16         //   testMethod:
17         //     The test method the test cases belong to.
18         //
19         //   factAttribute:
20         //     The fact attribute attached to the test method.
21         //
22         // Returns:
23         //     Returns zero or more test cases represented by the test method.
24         IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute);
25     }
26 }

  此時再回顧一下開始定義的RetryFact屬性標籤,爲它指定了自定義的Test Case Discoverer。so... ... 在xUnit.NetRunner運行Test Case時就能夠識別出來咱們所自定義的標籤了。另外,RetryFactDiscoverer採用了構造函數注入的方式獲取到了一個現實了IMessageSink接口的對象,這個對象是用來想Runner傳遞消息的會在消息總線的部分介紹。ide

(三)定義運行策略:XunitTestCase

  細心的同窗應該已經發現,上一部分Discover方法的返回值是一個可枚舉類型而且實現了IXunitTestCase接口的對象,xUnit.Net Framework 會以此調用接口的RunAsync方法。咱們的例子中返回了自定義的RetryTestCase對象,這一部分咱們就來看看它是如何實現的。Discoverer只是告訴xUnit.Net哪些方法是測試方法,而若是想要自定義測試方法運行的時機或者想在運行先後添加處理邏輯的話就須要建立自定義的TestCase類了。這裏咱們須要實現的邏輯就是根據用戶代碼在RetryFact中設置的運行次數來重複運行用例,代碼以下:函數

 1 namespace Demo.UnitTest.RetryFact
 2 {
 3     [Serializable]
 4     public class RetryTestCase : XunitTestCase
 5     {
 6         private int maxRetries;
 7 
 8         [EditorBrowsable(EditorBrowsableState.Never)]
 9         [Obsolete("Called by the de-serializer", true)]
10         public RetryTestCase() { }
11 
12         public RetryTestCase(
13             IMessageSink diagnosticMessageSink, 
14             TestMethodDisplay testMethodDisplay, 
15             ITestMethod testMethod, 
16             int maxRetries)
17             : base(diagnosticMessageSink, testMethodDisplay, testMethod, testMethodArguments: null)
18         {
19             this.maxRetries = maxRetries;
20         }
21 
22 
23         // This method is called by the xUnit test framework classes to run the test case. We will do the
24         // loop here, forwarding on to the implementation in XunitTestCase to do the heavy lifting. We will
25         // continue to re-run the test until the aggregator has an error (meaning that some internal error
26         // condition happened), or the test runs without failure, or we've hit the maximum number of tries.
27         public override async Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink,
28                                                         IMessageBus messageBus,
29                                                         object[] constructorArguments,
30                                                         ExceptionAggregator aggregator,
31                                                         CancellationTokenSource cancellationTokenSource)
32         {
33             var runCount = 0;
34 
35             while (true)
36             {
37                 // This is really the only tricky bit: we need to capture and delay messages (since those will
38                 // contain run status) until we know we've decided to accept the final result;
39                 var delayedMessageBus = new DelayedMessageBus(messageBus); 40 
41                 var summary = await base.RunAsync(diagnosticMessageSink, delayedMessageBus, constructorArguments, aggregator, cancellationTokenSource);
42                 if (aggregator.HasExceptions || summary.Failed == 0 || ++runCount >= maxRetries)
43                 {
44                     delayedMessageBus.Dispose();  // Sends all the delayed messages
45                     return summary;
46                 }
47 
48                 diagnosticMessageSink.OnMessage(new DiagnosticMessage("Execution of '{0}' failed (attempt #{1}), retrying...", DisplayName, runCount));
49             }
50         }
51 
52         public override void Serialize(IXunitSerializationInfo data)
53         {
54             base.Serialize(data);
55 
56             data.AddValue("MaxRetries", maxRetries);
57         }
58 
59         public override void Deserialize(IXunitSerializationInfo data)
60         {
61             base.Deserialize(data);
62 
63             maxRetries = data.GetValue<int>("MaxRetries");
64         }
65     }
66 }

  上面的代碼主要要注意如下幾點:

  • 自定義的TestCase類最好是繼承自XunitTestCase(若是有更深層次的要求能夠直接實現IXunitTestCase)
  • 重寫基類的RunAsync方法,該方法會在Runner運行Test Case的時候被調用。
  • 重寫Serialize / Deserialize 方法,像xUnit.Net上下文中添加對自定義屬性值的序列化/反序列化的支持。
  • 目前,無參構造函數RetryTestCase目前是必須有的(後續的版本中應當會移除掉)。不然,Runner會沒法構造無參的Case。

 最後,在RunAsync中,咱們根據用戶設置的次數運行測試用例。若是一直沒有成功,則會向消息接收器中添加一個錯誤的Message(該消息最終會經過消息總線返回給實際的Runner)。能夠看到,DelayedMessageBus (代碼中 Line38) 是咱們自定義的消息總線。

(四)與Runner交流:消息總線 - IMessageBus

  在測試用例被xUnit.Net對應的Runner運行的時候,Runner和測試框架的消息溝通是經過消息總線的形式來實現的,這也是不少相似系統都會提供的能力。IMessageBus中定義了向運行xUnit.Net測試用的Runner發送消息的接口方法QueueMessage:

 1 namespace Xunit.Sdk
 2 {
 3     // Summary:
 4     //     Used by discovery, execution, and extensibility code to send messages to
 5     //     the runner.
 6     public interface IMessageBus : IDisposable
 7     {
 8         // Summary:
 9         //     Queues a message to be sent to the runner.
10         //
11         // Parameters:
12         //   message:
13         //     The message to be sent to the runner
14         //
15         // Returns:
16         //     Returns true if discovery/execution should continue; false, otherwise.  The
17         //     return value may be safely ignored by components which are not directly responsible
18         //     for discovery or execution, and this is intended to communicate to those
19         //     sub-systems that that they should short circuit and stop their work as quickly
20         //     as is reasonable.
21         bool QueueMessage(IMessageSinkMessage message);
22     }
23 }

  這裏咱們自定義的消息總線以下:

 1     public class DelayedMessageBus : IMessageBus
 2     {
 3         private readonly IMessageBus innerBus;
 4         private readonly List<IMessageSinkMessage> messages = new List<IMessageSinkMessage>();
 5 
 6         public DelayedMessageBus(IMessageBus innerBus)
 7         {
 8             this.innerBus = innerBus;
 9         }
10 
11         public bool QueueMessage(IMessageSinkMessage message)
12         {
13             lock (messages)
14                 messages.Add(message);
15 
16             // No way to ask the inner bus if they want to cancel without sending them the message, so
17             // we just go ahead and continue always.
18             return true;
19         }
20 
21         public void Dispose()
22         {
23             foreach (var message in messages)
24                 innerBus.QueueMessage(message);
25         }
26     }

  這裏只是簡單的對隊列中的消息進行了暫存,實際的應用中應該會更復雜。

  到此爲止,咱們已經完成了自定義屬性標籤的全部的工做。如今系統中已經有了一個叫作RetryTestCase的標籤,你能夠用它來標記某個測試方法而且提供一個MaxRetries的值。當你運行測試用例的時候他會按照你設置的參數屢次運行被標記的測試方法,直到有一次成功或者運行次數超過了最大限制(若是用戶代碼設置的值小於3的狀況下,這裏默認會運行3次,Demo而已哈~~~),回顧一下本文開始的那個測試用例:

 1         public class RetryFactSample : IClassFixture<CounterFixture>
 2         {
 3             private readonly CounterFixture counter;
 4 
 5             public RetryFactSample(CounterFixture counter)
 6             {
 7                 this.counter = counter;
 8                 counter.RunCount++;
 9             }
10 
11             [RetryFact(MaxRetries = 5)]
12             public void IWillPassTheSecondTime()
13             {
14                 Assert.Equal(2, counter.RunCount);
15             }
16         }

  每運行一次RunCount會被加1,直到counter.RunCount == 5,運行結構以下:

 

總結:

  這一篇文章應該算是xUnit.Net中比較難理解的一部分。固然也算得上是個里程碑了,搞明白這一部分就至關於瞭解了一些xUnit.Net的設計和運行原理。也只有這樣纔有可能真的「玩轉」xUnit.Net。不然,僅僅是一個使用者而已,最後回顧一下本文:

  • 概述
  • 讓xUnit.Net識別你的測試Attribute
  • 定義運行策略:XunitTestCase
  • 與Runner交流:消息總線 - IMessageBus

小北De系列文章:

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

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

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

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

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