[小北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
声明:本博客原创文字只表明本人工做中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未受权,贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文链接。
相关文章
相关标签/搜索