在使用xUnit.Net Framework构建单元测试或自动化测试项目的时候,无论是针对一些比较耗费资源的对象亦或是为了支持Test case预设数据的能力,我们都需要有一些初始化或是清理相关的动作。在xUnit.Net中,提供了多种方式来满足我们的需要。还是照例看一下本文要讨论的内容:
- xUnit.Net 共享数据的方式(上)
- Test Case的构造函数 & IDisposable.Dispose(上)
- Class级别的Fixture : IClassFixture(上)
- Collection级别的Fixture : ICollectionFixture(下)
- 依赖注入以及输出日志(下)
本文我们只是讨论前三个议题,其他的议题我会在《xUnit.Net 之 Fixture(下)》中讨论。
首先,我们虚拟一个自动化测试中很容易遇到的一个场景。有四个Test case如下图所示:
可以看到,其中前三个Case(测试功能01,02,03)步骤如下,创建DB连接->打开浏览器->执行功能->关闭浏览器->释放数据库连接。而功能测试04仅仅是需要操作数据库而已,没有操作浏览器的需求。因此,不需要在浏览器中进行操作。对于这样的一个场景,直接能想到的方法是编写4个测试方法打上前面的[Fact]标签,每个方法中创建数据库连接,打开浏览器,操作,然后释放数据库,关闭浏览器。但这样的做法有很多的问题,比如会多次占用浏览器驱动和数据库连接这样的非托管资源。而打开数据库连接和浏览器驱动往往是比较耗时的操作,多次打开会无端的增加Test case的运行时间。
那么,如何来设计测试步骤呢?这里,我建议满足下面几个条件(当然也是为了讲解今天的内容):
- 在每个测试任务开始之前做一些数据的初始化工作。
- 打开一次浏览器,完成测试功能01,02,03之后,在关闭浏览器。在减少创建浏览器的开销的同时节省了测试时间
- 在应用程序级别统一创建数据库连接,Test Case 使用的数据库连接是同一份(或是统一管理的)。
(一)xUnit.Net 共享数据的方式
对于之前描述的业务场景,我们需要在每个Test Case执行前后,一组Test Case执行前后,所有Test Case执行前后这三个维度上添加自定义的操作。对应下来:
- 每个Test Case执行前后 : 在每个测试任务开始之前做一些数据的初始化工作。
- 一组Test Case执行前后 : 打开一次浏览器,完成测试功能01,02,03之后,再关闭浏览器。
- 所有Test Case执行前后 : 在应用程序级别统一创建数据库连接。
如图所示,CollectionFixture可以用于添加所有Test Case执行前后的一些操作(即例子中的创建和销毁数据库连接)。对于部分Case需要初始化浏览器,我们可以使用ClassFixture提供的功能。每个Case执行前后的操作我们可以使用测试类构造函数和IDisposable.Dispose来进行处理。下面我就逐一为大家讲解如何使用这些功能。
(二)Test Case的构造函数 & IDisposable.Dispose
如何在每个Test Case执行前后做处理?这应该是每个使用过单元测试框架的同学都知道的。多数的框架都是通过打标签的方式来提供类似功能的,例如:NUnit的[Setup]和[TearDown] , MSTest的[TestInitialize]和[TestCleanup]。而xUnit.Net提供了一种更加优雅的处理方式,就是利用构造函数以及IDisposable.Dispose方法来实现对应的功能。懂得一些面向对象的小伙伴也许会发现,这样的改进主要是为了支持依赖注入(而不是简单的省去了标签而已)。这也为我后续的文章中要讲到的许多功能的注入实现提供了可能。讲了这么多理论,先上一段Code:
1 namespace Demo.UnitTest.Lesson03_Fixture
2 {
3 public class SharedContext_Constructor : IDisposable
4 {
5 private ITestOutputHelper _output;
6 public SharedContext_Constructor(ITestOutputHelper output)
7 {
8 this._output = output;
9 _output.WriteLine("Execute constructor!");
10 }
11
12 #region Test case
13 [Fact(DisplayName = "SharedContext.Constructor.Case01")]
14 public void TestCase01()
15 {
16 _output.WriteLine("Execute case 01!");
17 }
18
19 [Fact(DisplayName = "SharedContext.Constructor.Case02")]
20 public void TestCase02()
21 {
22 _output.WriteLine("Execute case 02!");
23 }
24
25 [Fact(DisplayName = "SharedContext.Constructor.Case03")]
26 public void TestCase03()
27 {
28 _output.WriteLine("Execute case 03!");
29 }
30 #endregion
31
32 public void Dispose()
33 {
34 _output.WriteLine("Execute dispose!");
35 }
36 }
37 }
代码中的ITestOutputHelper就是通过构造函数注入的方式为我们提供了输出Log的能力(这个下一篇的文章我会为大家讲解),这里你只需要知道他是可以输出一些日志的即可。上面的Code中,有3个Case,Case执行的时Runner会在执行每个Case前后分别调用测试类的构造函数和对应的Dispose方法。输出如下,我们可以看到测试类构造函数和Dispose方法在每一个Case执行前后都被执行。
(三)Class级别的Fixture : IClassFixture
ok,现在我们考虑前文中提到的问题二:仅仅打开一次浏览器,完成测试功能01,02,03之后,再关闭浏览器。xUnit.Net为我们提供了基于类级别的Fixture,即IClassFixture。IClassFixture是一个泛型接口(标记接口,没有任何需要实现的方法),接受一个类型。该类型的构造函数会在测试类中的第一个Test Case运行之前被调用。而其IDisposable.Dispose方法会在测试类中最后一个测试方法执行完成之后被执行。IClassFixture定义如下:
1 namespace Xunit
2 {
3 public interface IClassFixture<TFixture> where TFixture : class
4 {
5 }
6 }
如何使用IClassFixture呢?步骤如下:
Step 01 : 创建自定义的Fixture类,添加构造函数和IDisposable接口的实现方法。本文主要是讲解xUnit.Net的使用,因此示例代码中我没有给出创建浏览器驱动的具体代码(这部分内容可以参见我的另一个系列《[小北De编程手记] : Selenium For C# 教程》),只是添加了ExecuteCount属性用于标记执行次数,代码如下:
1 public class SingleBrowserFixture : IDisposable
2 {
3 public int UserId { get; set; }
4 public string UserName { get; set; }
5 public static int ExecuteCount;
6
7 public SingleBrowserFixture()
8 {
9 this.UserId = 1;
10 this.UserName = "North";
11 ExecuteCount++;
12
13 //打开浏览器...
14 }
15
16 public void Dispose()
17 {
18 //关闭浏览器...
19 }
20 }
Step 02 :创建具体的测试类,并继承 IClassFixture<SingleBrowserFixture>(注意:我们用接口标记需要使用哪一个类)
Step 03 :在测试类中获取Fixture对象,xUnit.Net 用构造函数注入的方式提供了获取IClassFixture标记对象的方法。我们可以在测试类的构造函数中添加对应的注入参数来获取Fixture,这样的设计使得我们在测试类中所有的测试用例中共享一些Context数据,xUnit.Net执行测试用例的时候会自动识别构造参数的类型是否和IClassFixture所标记的类型是否匹配。代码如下:
1 public class SharedContext_ClassFixture : IClassFixture<SingleBrowserFixture>
2 {
3 ITestOutputHelper _output;
4 SingleBrowserFixture _fixture;
5 static int _count;
6 public SharedContext_ClassFixture(ITestOutputHelper output, SingleBrowserFixture fixture)
7 {
8 _output = output;
9 _fixture = fixture;
10 _count++;
11 }
12 #region Test case
13 [Fact(DisplayName = "SharedContext.ClassFixture.Case01")]
14 public void TestCase01()
15 {
16 _output.WriteLine("Execute case 01! Current User:[{0}]-{1}", _fixture.UserId, _fixture.UserName);
17 _output.WriteLine("Execute count! Constructor:[{0}] , ClassFixture:[{1}]", _count, SingleBrowserFixture.ExecuteCount);
18
19 }
20
21 [Fact(DisplayName = "SharedContext.ClassFixture.Case02")]
22 public void TestCase02()
23 {
24 _output.WriteLine("Execute case 01! Current User:[{0}]-{1}", _fixture.UserId, _fixture.UserName);
25 _output.WriteLine("Execute count! Constructor:[{0}] , ClassFixture:[{1}]", _count, SingleBrowserFixture.ExecuteCount);
26 }
27 #endregion Test case
28 }
代码中可以看到,我用_count 标记了测试类的执行次数,用ExecuteCount标记Fixture类的执行次数,看下运行结果:
可以看到,测试类的构造被执行了2次(也就是每个测试用例执行的时候都会执行一次),而ClassFixture标记的测试类中的构造函数只是被执行了一次。IDisposable.Dispose 也具有相同的逻辑。
下一篇,为大家介绍:
- Collection级别的Fixture : ICollectionFixture(下)
- 依赖注入以及输出日志(下)
小北De系列文章:
《[小北De编程手记] : Selenium For C# 教程》
《[小北De编程手记]:C# 进化史》(未完成)
《[小北De编程手记]:玩转 xUnit.Net》(未完成)
Demo地址:https://github.com/DemoCnblogs/xUnit.Net
上一篇文章《[小北De编程手记] : Lesson 03 玩转 xUnit.Net 之 Fixture(上)》向大家介绍了xUnit.Net 共享数据的方式、Test Case的构造函数 & IDisposable.Dispose、Class级别的Fixture : IClassFixture。这一篇,我们接着讲解后面的内容,回顾一下本文要讨论的内容:
- xUnit.Net 共享数据的方式(上)
- Test Case的构造函数 & IDisposable.Dispose(上)
- Class级别的Fixture : IClassFixture(上)
- Collection级别的Fixture : ICollectionFixture(下)
- 依赖注入以及输出日志(下)
(四)Collection级别的Fixture : ICollectionFixture
回想一下上一篇中我们虚拟的应用场景。其中,关于问题三:“在应用程序级别统一创建数据库连接,Test Case 使用的数据库连接是同一份(或是统一管理的)”。 针对这一需求的实现,我们可以使用xUnit.Net的ICollectionFixture来实现。Collection级别的Fixture为我们提供了可以在多个测试类之间数据共享的能力。包含在同一个Collection之下的所有测试用例共享一份上下文数据。下面我们就来动手实现一下虚拟场景问题三之中的那个功能吧。
Step 01:定义CollectionFixture(Demo中的DatabaseFixture)
与ClassFixture类似,自定义的CollectionFixture类,需要完成其构造函数 & IDisposable.Dispose的定义。而CollectionFixture类的构造和Dispose方法最终会在所有被标记使用该Collection的Test Class对应的Case执行前后被调用。即所有标记使用该Collection的测试方法运行之前会执行CollectionFixture的构造函数。所有标记使用该Collection的测试方法全部运行完毕之后会执行CollectionFixture的IDisposable.Dispose函数。我们定义一个DatabaseFixture,代码如下:
1 public class DatabaseFixture : IDisposable
2 {
3 public object DatabaseContext { get; set; }
4
5 public static int ExecuteCount { get; set; }
6
7 public DatabaseFixture()
8 {
9 ExecuteCount++;
10 //初始化数据连接
11 }
12
13 public void Dispose()
14 {
15 //销毁数据连接
16 }
17 }
代码中,省略了得创建和销毁数据库连接的Code。只是使用了一个object类型的属性来表示数据库上下文,并且创建了一个静态变量ExecuteCount用于标记构造函数的使用频率。
Step 02:定义Collection。
对于ClassFixture而言,因为是基于Class级别的数据共享。so... ... xUnit.Net提供了直接用类继承IClassFixture接口并结合构造函数注入的方式优雅的实现了数据共享的功能。而对于Collection(一组类)的数据共享又该如何实现呢?先看一下示例代码:
1 /// <summary>
2 /// 定义Collection名称,标明使用的Fixture
3 /// </summary>
4 [CollectionDefinition("DatabaseCollection")]
5 public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
6 {
7 }
可以看到,我们定义了一个没有任何内容的类DatabaseCollection,该类的主要功能是定义了一个名字为“DatabaseCollection”(此名称可以和类名不同)的Collection,并指明该Collection所对应了Fixture。需要说明的是ICollectionFixture和IClassFixture一样是一个泛型标记接口(即没有任何需要实现的方法,只是用来标记对应的Fixture的类型)。而定义Collection代码中使用了CollectionDefinition标签,其定义如下:
1 namespace Xunit
2 {
3 // Summary:
4 // Used to declare a test collection container class. The container class gives
5 // developers a place to attach interfaces like Xunit.IClassFixture<TFixture>
6 // and Xunit.ICollectionFixture<TFixture> that will be applied to all tests
7 // classes that are members of the test collection.
8 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
9 public sealed class CollectionDefinitionAttribute : Attribute
10 {
11 // Summary:
12 // Initializes a new instance of the Xunit.CollectionDefinitionAttribute class.
13 //
14 // Parameters:
15 // name:
16 // The test collection name.
17 public CollectionDefinitionAttribute(string name);
18 }
19 }
被CollectionDefinition标记的Class在运行时会被xUnit.Net框架实例化为一个对象,该对象将用于标记其他的Class(有兴趣的话可以去GitHub看看xUnit.Net的源代码)。这里需要一个CollectionName作为参数,该参数将会用标记那些需要使用这个CollectionFixture的类。
Step 03:用Collection来标记需要使用Fixtrue的测试类。
xUnit.Net提供了Collection类,它的作用是用来指明测试类需要使用哪个Collection的。所有被标记了Collection测试类中的测试方法在其运行之前会调用一次对应的CollectionFixture的构造函数,所有方法运行完毕之后会调用一次CollectionFixture的IDisposable.Dispose函数(如果定义了的话)。值得注意的是测试类中依旧是通过构造函数注入的方式获取DatabaseFixture实例对象的。那么,我们来看一下Demo:
1 [Collection("DatabaseCollection")]
2 public class SharedContext_CollectionFixture_01
3 {
4 private DatabaseFixture _dbFixture;
5 private ITestOutputHelper _output;
6 public SharedContext_CollectionFixture_01(ITestOutputHelper output, DatabaseFixture dbFixture)
7 {
8 _dbFixture = dbFixture;
9 _output = output;
10 }
11
12 [Fact(DisplayName = "SharedContext.CollectionFixture.Case01")]
13 public void TestCase01()
14 {
15 _output.WriteLine("Execute CollectionFixture case 01!");
16 _output.WriteLine("DatabaseFixture ExecuteCount is : {0}", DatabaseFixture.ExecuteCount);
17 }
18 }
19
20 [Collection("DatabaseCollection")]
21 public class SharedContext_CollectionFixture_02
22 {
23 private DatabaseFixture _dbFixture;
24 private ITestOutputHelper _output;
25 public SharedContext_CollectionFixture_02(DatabaseFixture dbFixture, ITestOutputHelper output)
26 {
27 _dbFixture = dbFixture;
28 _output = output;
29 }
30
31 [Fact(DisplayName = "SharedContext.CollectionFixture.Case02")]
32 public void TestCase01()
33 {
34 _output.WriteLine("Execute CollectionFixture case 02!");
35 _output.WriteLine("DatabaseFixture ExecuteCount is : {0}", DatabaseFixture.ExecuteCount);
36 }
37 }
Dome中定义了两个测试类,每个测试类中有一个测试方法,并用Collection指明了需要使用的Collection的名称。运行结果如下:
可以看到,DatabaseFixture中的构造函数只是被执行了一次(IDisposable.Dispose也有相同的逻辑)。因此,实际的单元测试中,我们可以此处构建、管理数据库连接以节省资源的开销。
(五)依赖注入以及输出日志
依赖注入是一个重要的OOP的法则,用来削减计算机程序的耦合问题。如今已成为许多不同领域软件框架的核心。关于依赖注入的概念,我想大家都不会陌生。这里我列出了几种依赖注入的主要方式:
- 类型1 (基于接口): 可服务的对象需要实现一个专门的接口,该接口提供了一个对象,可以重用这个对象查找依赖(其它服务)。
- 类型2 (基于setter): 通过JavaBean的属性(setter方法)为可服务对象指定服务。
- 类型3 (基于构造函数): 通过构造函数的参数为可服务对象指定服务。
这里谈到依赖注入,主要是想跟大家分享本人对xUnit.Net的设计理念的一点点理解。我在第一篇xUnit.Net系列文章《[小北De编程手记] : Lesson 01 玩转 xUnit.Net 之 概述》中曾提到过:xUnit.Net的一个改进就是在处理每个Test Case的初始化和清理方法时不再使用属性标签来标记,而是采用了构造函数和IDisposable.Dispose方法。这样做的一个直接好处就是使得依赖注入更容易的运用于xUnit.Net之中。前面例子中各个级别的Fixture,日志对象... ...都是通过依赖注入的方式简单,优雅的被我们所获取到。而对于日志对象,使用者也无需去关注它会输出到哪里(这个是由运行Case的工具<即Runner>决定),我们甚至不用关心它是如何被实例化。当使用不同的Runner运行Case时,Runner会针对xUnit.net的接口去实现一套属于自己的输出方式。下面我们来回顾一下输出接口以及它的使用方式:
1 namespace Xunit.Abstractions
2 {
3 public interface ITestOutputHelper
4 {
5 void WriteLine(string message);
6 void WriteLine(string format, params object[] args);
7 }
8 }
可以看到,ITestOutputHelper定义了两个输出方法,使用者可以通过下面的方式(构造函数注入)获取到运行时Runner提供的输出对象。而关于对象的实例化,管理等操作都是由运行Case的Runner(程序)来管理的。后面我会为大家讲解如何自定义Runner以及自定义Runner的意义所在,这里就不再赘述了。
1 public class SharedContext_ClassFixture : IClassFixture<SingleBrowserFixture>
2 {
3 ITestOutputHelper _output;
4 public SharedContext_ClassFixture(ITestOutputHelper output , SingleBrowserFixture fixture)
5 {
6 _output = output;
7 }
8 #region Test case
9 [Fact(DisplayName = "SharedContext.ClassFixture.Case01")]
10 public void TestCase01()
11 {
12 _output.WriteLine("Log here");
13 }
14 #endregion Test case
15 }
日志对象本身的使用很简单,单独拉出来讲是为了向大家展示xUnit.Net设计的工匠精神(更靠近设计者的意图)。很多框架级别的改变虽小(NUnit使用属性标签标记初始化方法,而xUnit.Net使用构造函数),但是用意颇深。so... ... 我们就慢慢体会吧~~~
这两篇文章主要和大家探讨了以下问题:
- xUnit.Net 共享数据的方式
- Test Case的构造函数 & IDisposable.Dispose
- Class级别的Fixture : IClassFixture
- Collection级别的Fixture : ICollectionFixture
- 依赖注入以及输出日志
关于的xUnit.Net Fixture的基本使用就先介绍到这里了,下一篇为大家讲解一下如何在Fixture的层面上扩展xUnit.Net的功能。到时候,让我们一起来看看xUnit.Net在可扩展性方面有何过人之处?
小北De系列文章:
《[小北De编程手记] : Selenium For C# 教程》
《[小北De编程手记]:C# 进化史》(未完成)
《[小北De编程手记]:玩转 xUnit.Net》(未完成)
Demo地址:https://github.com/DemoCnblogs/xUnit.Net
