xUnit.Net自己提供了標記測試方法的標籤Fact和Theory。在前面的文章《Lesson 02 玩轉 xUnit.Net 之 基本UnitTest & 數據驅動》中,也對它們作了詳細的介紹。這一篇,來分享一個高級點的主題:如何擴展標籤?仍是老規矩,看一下議題:html
這一篇有一些不大容易理解的東東。所以,我是默認讀者已經讀過以前的五篇文章(或者已經充分的瞭解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所設置的次數來重複執行被標記的測試用例。自定義運行標籤主要有下面幾個步驟:編程
該功能也是xUnit.Net官網上提供的示例代碼之一。有興趣的小夥伴能夠去看看,那裏還有不少其餘的Demo。是否是以爲這個功能很不錯呢?接下來我就開始向你們介紹如何實現它吧。app
最開始固然是須要建立一個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
細心的同窗應該已經發現,上一部分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 }
上面的代碼主要要注意如下幾點:
最後,在RunAsync中,咱們根據用戶設置的次數運行測試用例。若是一直沒有成功,則會向消息接收器中添加一個錯誤的Message(該消息最終會經過消息總線返回給實際的Runner)。能夠看到,DelayedMessageBus (代碼中 Line38) 是咱們自定義的消息總線。
在測試用例被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。不然,僅僅是一個使用者而已,最後回顧一下本文:
小北De系列文章:
《[小北De編程手記] : Selenium For C# 教程》
《[小北De編程手記]:C# 進化史》(未完成)
《[小北De編程手記]:玩轉 xUnit.Net》(未完成)
Demo地址:https://github.com/DemoCnblogs/xUnit.Net