Xunit测试使用小结
1.Attribute
1.1FactAttribute
Fact中共有3个属性,以下是Fact类的代码
public class FactAttribute : Attribute { public FactAttribute(); public virtual string DisplayName { get; set; } public virtual string Skip { get; set; } public virtual int Timeout { get; set; } }
1.1.1 DisplayName
属性DisplayName,其作用是设置名称。
在重新设置过名称后,右侧的管理器里就显示设置过的名称
1.1.2 Skip
属性Skip,其作用标记上后就直接跳过,不会执行当前用例,且会以黄色三角标识。
1.1.3 Timeout
属性Timeout作用针对Task设置一个超时时间,以毫秒为单位,当执行的时间大于设定的时间,即算测试用例执行失败。
示例
以下是3个测试用例,分别为void,task,task,timeout时间均设置为50ms
[Fact(Timeout = 50)] public void Test1() { Task.Delay(100).Wait(); } [Fact(Timeout =50)] public async Task Test2() { await Task.Delay(100); } [Fact(Timeout = 50)] public async Task Test3() { await Task.Delay(20); }
以下是运行的结果
1.2 TraitAttribute
Trait属性用来标识不同的特性分组,可用于标识class和method
示例
目前有两个测试类UnitTest1,UnitTest2
public class UnitTest1 { [Fact] public void Test1() { } [Fact] public void Test2() { } [Fact] public void Test3() { } } public class UnitTest2 { [Fact] public void Test1() { } [Fact] public void Test2() { } }
一开始排序是根据项目,命名空间,类来排序的
现在设置trait值进行更改,对UnitTest1和UnitTest2的class和method上进行设置,对UnitTest1的class设置Trait为trait1,其中的方法Test2设置trait为Trait2,方法Test3设置Trait为trait3。对UnitTest2的class设置Trait为trait2,其中的方法Test2设置Trait为trait3
[Trait("trait1","item1")] public class UnitTest1 { [Fact] public void Test1() { } [Fact] [Trait("trait2","item2")] public void Test2() { } [Fact] [Trait("trait3","item3")] public void Test3() { } } [Trait("trait2","item2")] public class UnitTest2 { [Fact] public void Test1() { } [Fact] [Trait("trait3","item3")] public void Test2() { } }
选择分组依据,按照按特征分组
分组的结果如下,可以看到对class进行trait标记时,那么这个class下面的测试用例会同样打上同样的trait标记,对method进行trait标记时,那么这个method除了本身的trait标记外也会存在class的trait标记,对同一个class或method进行trait标记可以允许进行多次标记
1.3 TheoryAttribute
Theory表示执行相同方法,可具有不同输入参数的测试套件。从下图中,可知Theory是基础于Fact的,Fact中的三个参数同样对Theory有效,可以用Theory来代替Fact
public class TheoryAttribute : FactAttribute { public TheoryAttribute(); }
使用Theory时,需要搭配DataAttribute使用,DataAttribute就是编写不同输入参数的数据源。
1.4 DataAttribute
DataAttribute共一个Skip属性和一个返回数据给theory的方法
public abstract class DataAttribute : Attribute { /// <summary> /// Marks all test cases generated by this data source as skipped. /// </summary> public virtual string Skip {get;set;} /// <summary> /// Returns the data to be used to test the theory. /// </summary> public abstract IEnumerable<object[]> GetData(MethodInfo testMethod); }
搭配Theory使用的DataAttribute有3种:
1.InlineDataAttribute
2.MemberDataAttribute
3.继承DataAttribute的自定义class
1.4.1 InlineDataAttribute
InlineDataAttribute代码如下,在InlineDataAttribute构造函数里需要传入一个类型为object的一维数组作为测试数据来使用,每一个InlineDataAttribute对应着一组测试数据
public sealed class InlineDataAttribute : DataAttribute { private readonly object[] data; public InlineDataAttribute(params object[] data) { this.data = data; } public override IEnumerable<object[]> GetData(MethodInfo testMethod) { return new object[1][] { data }; } }
例子如下
public class UnitTest1 { [Theory] [InlineData(1, 2, 3)] [InlineData(1, 2, 4)] public void Test1(int num1, int num2, int num3) { Assert.Equal(num3, num1 + num2); } [Theory] [InlineData("a", "b")] [InlineData("a", "a")] public void Test2(string str1, string str2) { Assert.Equal(str1, str2); } }
运行结果如下
针对同一个测试用例,可以设计不同的入参传入,但注意的是InlineData的构造函数的参数个数和类型要和测试用例的入参个数和类型要对应。
1.4.2 MemberDataAttribute
MemberDataAttribute继承于MemberDataAttributeBase,且主要的代码实现都是在MemberDataAttributeBase里面。获取数据源的途径有3种,可以从Field字段,Property属性和Method方法3种成员获取数据,这3种成员返回结果必须为IEnumerable<object[]>且为静态的(注:数据可以从文本,数据库其他地方来,演示数据为了方便简单写)
1.4.2.1从Property中获取数据
例子如下:
public class UnitTest1 { public static IEnumerable<object[]> PropertyData { get { return new List<object[]> { new object[]{"a","a"}, new object[]{"a","b"} }; } } [Theory] [MemberData(nameof(UnitTest1.PropertyData),MemberType=typeof(UnitTest1))] public void Test1(string str1,string str2) { Assert.Equal(str1, str2); } }
运行结果:
1.4.2.2从Field中获取数据
例子如下:
public static IEnumerable<object[]> FileData = new List<object[]> { new object[]{ 1,2,3,},new object[]{4,5,6 } }; [Theory] [MemberData(nameof(FileData))] public void Test1(int num1, int num2,int num3) { Assert.Equal(num1+num2, num3); }
运行结果如下
1.4.2.3从method中获取数据
例子如下:
public static IEnumerable<object[]> GetData() { return new List<object[]> { new object[] { true },new object[] { false } }; } [Theory] [MemberData(nameof(GetData))] public void Test1(bool condition) { Assert.True(condition); }
运行结果:
1.4.3 继承DataAttribute的自定义class
可以自定义继承DataAttribute的数据类,实现GetData方法即可,供多个类使用。
例子如下:
public class TestData : DataAttribute { public override IEnumerable<object[]> GetData(MethodInfo testMethod) { return new List<object[]> { new object[] { true }, new object[] { false } }; } } public class UnitTest1 { [Theory] [TestData] public void Test1(bool condition) { Assert.True(condition); } } public class UnitTest2 { [Theory] [TestData] public void Test1(bool condition) { Assert.True(condition); } }
运行结果如下:
1.4.4ClassDataAttribute
ClassDataAttribute用于获取测试数据源,其用法为传入一个数据源的type
public class ClassDataAttribute : DataAttribute { public ClassDataAttribute(Type @class); public Type Class { get; } public override IEnumerable<object[]> GetData(MethodInfo testMethod); }
1.4.4.1从TheoryData中获取数据源
TheoryData可以有0-10个参数可供选择
例子如下:
public class TestData : TheoryData<int,int> { ///在构造函数内添加数据 public TestData() { Add(1, 1); Add(1, 2); } } [Theory] [ClassData(typeof(TestData))] public void Test1(int num1,int num2) { Assert.Equal(num1, num2); }
1.4.4.2从继承IEnumerable<object[]>的自定义class获取数据源
自定义实现IEnumerable<object[]>的类
例子如下:
public class TestIEnumerableData : IEnumerable<object[]> { private readonly List<object[]> _data = new List<object[]> { new object[] { true }, new object[] { false } }; public IEnumerator<object[]> GetEnumerator() => _data.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } [Theory] [ClassData(typeof(TestData))] public void Test1(int num1,int num2) { Assert.Equal(num1, num2); }
1.5 TestCaseOrdererAttribute
TestCaseOrderer是可用来自定义测试类中的测试用例的执行顺序,自定义要对ITestCaseOrderer实现
例子如下:
设定3个测试用例,为了使测试用例顺序更明显,每个测试用例执行2s,点击运行
public class UnitTest1 { [Fact] public void Test1() { Thread.Sleep(2000); } [Fact] public void Test2() { Thread.Sleep(2000); } [Fact] public void Test3() { Thread.Sleep(2000); } }
运行结果则是按照方法顺序依次执行
再自定义测试用例顺序,将测试用例根据名称反过来执行
public class TestOrder : ITestCaseOrderer { public IEnumerable<TTestCase> OrderTestCases<TTestCase>(IEnumerable<TTestCase> testCases) where TTestCase : ITestCase { return testCases.OrderByDescending(i => i.TestMethod.Method.Name); } } //第一个为排序类完整名称,第二个是排序类所在的程序集 [TestCaseOrderer("XunitTest.TestOrder", "XunitTest")] public class UnitTest1 { [Fact] public void Test1() { Thread.Sleep(2000); } [Fact] public void Test2() { Thread.Sleep(2000); } [Fact] public void Test3() { Thread.Sleep(2000); } }
运行结果如下
1.6 CollectionAttribute
Collect的作用是可将多个测试用例class标记为同一个集合,不同的集合会并行执行,但标记为同一个集合后则集合内的测试用例则是按顺序依次执行。
以下是CollectAttribute代码,构造函数能设置collection的名称
public sealed class CollectionAttribute : Attribute { public CollectionAttribute(string name); }
例子如下:
设置3个测试类,每个类里边有2个测试用例
public class UnitTest1 { [Fact] public void Test1() { Thread.Sleep(1000); } [Fact] public void Test2() { Thread.Sleep(2000); } } public class UnitTest2 { [Fact] public void Test1() { Thread.Sleep(2000); } [Fact] public void Test2() { Thread.Sleep(2000); } } public class UnitTest3 { [Fact] public void Test1() { Thread.Sleep(1000); } [Fact] public void Test2() { Thread.Sleep(2000); } }
在3个测试类没有设置CollectionAttribute的时候,点击运行,三个类会并行执行测试用例
运行结果如下:
对3个测试类中的前两个类设置同一个集合名,第三个不设置,再次点击运行,三个类中的前两个类中的测试用例按排序依次执行,和第三个类的测试用例并行执行。
[Collection("group1")] public class UnitTest1 { [Fact] public void Test1() { Thread.Sleep(1000); } [Fact] public void Test2() { Thread.Sleep(2000); } } [Collection("group1")] public class UnitTest2 { [Fact] public void Test1() { Thread.Sleep(2000); } [Fact] public void Test2() { Thread.Sleep(2000); } } [Collection("group2")] public class UnitTest3 { [Fact] public void Test1() { Thread.Sleep(1000); } [Fact] public void Test2() { Thread.Sleep(2000); } }
运行结果如下
2.Log
有一个ITestOutputHelper用来输出自定义信息,使用时在构造函数内加入即可
public interface ITestOutputHelper { //直接输出 void WriteLine(string message); //按格式输出 void WriteLine(string format, params object[] args); }
例子如下:
public class UnitTest1 { private readonly ITestOutputHelper _outputHelper; public UnitTest1(ITestOutputHelper testOutputHelper) { _outputHelper = testOutputHelper; } [Fact] public void Test2() { _outputHelper.WriteLine("log output"); } }
运行结果:
3.Fixture
项目中用的是api,通过模拟http请求来进行测试,需要在测试中构建一个相同环境的测试服务端。在创建测试用例时,如果对每个测试用例都进行构造一个服务端,则会延长测试时间和浪费资源,这时候就可以采用同一类或者同一个组使用共享上下文的方法进行。
3.1IClassFixture
实现class级别的共享上下文,构造共享上下文例子如下。
3.1.1构造startup项
根据项目的startup针对测试服务端可另行构造一个配置的startup项
public class TestStartUp { public TestStartup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); //项目中的配置项 } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //项目中的中间件 app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }
3.1.2构造类作为上下文
public class TestClassFixture:IDisposable { public TestServer Server { get; set; } public HttpClient HttpClient { get; set; } public IConfiguration Config { get; set; } //记录实例化的次数 public static int runtime; public TestClassFixture() { //具体项目根据program中的配置进行 var builder=new WebHostBuilder().UseStartup<TestStartup>(); Server=new TestServer(builder); HttpClient = Server.CreateClient(); //在测试中有用到项目的一些配置项,可以构造一个配置系统,将内容放到配置系统内,在测视类里面直接获取 Config = new ConfigurationBuilder() .AddJsonFile("xxx") .AddXmlFile("xxx") .Build(); //客户端需要添加请求头时 //ttpClient.DefaultRequestHeaders中添加内容 runtime++; } //执行完后的动作 public void Dispose() { HttpClient.Dispose(); Server.Dispose(); } }
3.1.3使用上下文
实现测试类时,也可以实现IDisposable,在IDisposable里面处理一些测试过的数据。TestClassFixture会在执行测试用例前进行初始化,在执行完最后的测试用例后销毁,为了使得效果更明显,加入日志输出,省略了httpclient调api的过程。
public class UnitTest1:IClassFixture<TestClassFixture>,IDisposable { private readonly ITestOutputHelper _outputHelper; private readonly TestClassFixture _fixture; public UnitTest1(ITestOutputHelper testOutputHelper, TestClassFixture fixture) { _outputHelper = testOutputHelper; _fixture = fixture; _outputHelper.WriteLine("实例化 UnitTest1"); } [Fact] public void Test1() { //取配置系统数据的用法 //_fixture.Config.GetSection("") _outputHelper.WriteLine("运行 test1"); _outputHelper.WriteLine($"TestClassFixture实例化次数:{TestClassFixture.runtime}"); } [Fact] public void Test2() { _outputHelper.WriteLine("运行 test2"); _outputHelper.WriteLine($"TestClassFixture实例化次数:{TestClassFixture.runtime}"); } //可以在此处理一些用过的数据,避免干扰其他测试用例中的数据 public void Dispose() { _outputHelper.WriteLine("销毁 UnitTest1"); } } public class UnitTest2:IClassFixture<TestClassFixture>,IDisposable { private readonly ITestOutputHelper _outputHelper; private readonly TestClassFixture _fixture; public UnitTest1(ITestOutputHelper testOutputHelper, TestClassFixture fixture) { _outputHelper = testOutputHelper; _fixture = fixture; _outputHelper.WriteLine("实例化 UnitTest2"); } [Fact] public void Test1() { //取配置系统数据的用法 //_fixture.Config.GetSection("") _outputHelper.WriteLine("运行 test1"); _outputHelper.WriteLine($"TestClassFixture实例化次数:{TestClassFixture.runtime}"); } public void Dispose() { _outputHelper.WriteLine("销毁 UnitTest2"); } }
执行结果如下:
由结果可以看出,对于同一个测试类,每执行一个测试用例,会经历实例化,运行,销毁的步骤,但上下文内容实例化一次。但不同的测试类之间会重新实例化上下文内容。
3.2ICollectionFixture
实现collection级别的共享上下文,用法同上面的相同,IClassFixture是以class为界限,ICollectionFixture则是以标记为同一个集合名称的为界限进行实例化。
实例如下:
3.2.1申明使用对象
申明名为group1的集合使用TestClassFixture作为上下文
[CollectionDefinition("group1")] public class TestCollectionFixture: ICollectionFixture<TestClassFixture> { }
3.2.2使用上下文
代码与IClassFixture有区别,用CollectionAttribute代替了原来要实现的IClassFixture
[Collection("group1")] public class UnitTest1:IDisposable { private readonly ITestOutputHelper _outputHelper; private readonly TestClassFixture _fixture; public UnitTest1(ITestOutputHelper testOutputHelper, TestClassFixture fixture) { _outputHelper = testOutputHelper; _fixture = fixture; _outputHelper.WriteLine("实例化 UnitTest1"); } [Fact] public void Test1() { //取配置系统数据的用法 //_fixture.Config.GetSection("") _outputHelper.WriteLine("运行 test1"); _outputHelper.WriteLine($"TestClassFixture实例化次数:{TestClassFixture.runtime}"); } [Fact] public void Test2() { _outputHelper.WriteLine("运行 test2"); _outputHelper.WriteLine($"TestClassFixture实例化次数:{TestClassFixture.runtime}"); } //可以在此处理一些用过的数据,避免干扰其他测试用例中的数据 public void Dispose() { _outputHelper.WriteLine("销毁 UnitTest1"); } } [Collection("group1")] public class UnitTest2:IDisposable { private readonly ITestOutputHelper _outputHelper; private readonly TestClassFixture _fixture; public UnitTest1(ITestOutputHelper testOutputHelper, TestClassFixture fixture) { _outputHelper = testOutputHelper; _fixture = fixture; _outputHelper.WriteLine("实例化 UnitTest2"); } [Fact] public void Test1() { //取配置系统数据的用法 //_fixture.Config.GetSection("") _outputHelper.WriteLine("运行 test1"); _outputHelper.WriteLine($"TestClassFixture实例化次数:{TestClassFixture.runtime}"); } public void Dispose() { _outputHelper.WriteLine("销毁 UnitTest2"); } }
运行结果如下:
由结果可以看到,相同collection名称的测试用例执行过程中只实例化了一次上下文。