关于《玩转 xUnit.Net》系列文章,我想跟大家分享的不是简单的运行一下测试用例或是介绍一下标签怎么使用(这样的文章网上很多)。上一篇《Lesson 01 玩转 xUnit.Net 之 概述》介绍xUnit.Net的一些基本概念。从这一篇开始我将会为大家逐一展示xUnit.Net的强大之处。还是先看一下本文要讨论的内容:
这里我先回顾一下前一篇文章的测试用例:
1 using System; 2 using System.Collections.Generic; 3 using Xunit; 4 5 public class EqualExample 6 { 7 [Fact] 8 public void EqualStringIgnoreCase() 9 { 10 string expected = "TestString"; 11 string actual = "teststring"; 12 13 Assert.False(actual == expected); 14 Assert.NotEqual(expected, actual); 15 Assert.Equal(expected, actual, StringComparer.CurrentCultureIgnoreCase); 16 } 17 }
你可能已经发现,xUnit.Net的中用来标记测试方法的attribute是[Fact],而不是一个像类似[Test]这样更传统的标记名称。xUnit.Net 包含了两种主要的单元测试方式:Fact 和 Theory,这两种方式的不同如下:
首先,我们来看一下Fact标签的结构:
1 // Summary: 2 // Attribute that is applied to a method to indicate that it is a fact that 3 // should be run by the test runner. It can also be extended to support a customized 4 // definition of a test method. 5 [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 6 [XunitTestCaseDiscoverer("Xunit.Sdk.FactDiscoverer", "xunit.execution.{Platform}")] 7 public class FactAttribute : Attribute 8 { 9 public FactAttribute(); 10 11 // Summary: 12 // Gets the name of the test to be used when the test is skipped. Defaults to 13 // null, which will cause the fully qualified test name to be used. 14 public virtual string DisplayName { get; set; } 15 // 16 // Summary: 17 // Marks the test so that it will not be run, and gets or sets the skip reason 18 public virtual string Skip { get; set; } 19 }
除了构造函数之外,该Attribute还提供了两个属性。
1 [Fact(DisplayName = "Lesson02.Demo01")] 2 public void Demo01_Fact_Test() 3 { 4 int num01 = 1; 5 int num02 = 2; 6 Assert.Equal<int>(3, num01 + num02); 7 } 8 9 [Fact(DisplayName = "Lesson02.Demo02", Skip = "Just test skip!")] 10 public void Demo02_Fact_Test() 11 { 12 int num01 = 1; 13 int num02 = 2; 14 Assert.Equal<int>(3, num01 + num02); 15 }
对于上面的两个测试用例,运行结果如下。可以看到两个测试用例的名称均显示为DisplayName对用的属性名称,而设置了Skip属性的Unit Test没有被执行。
关于数据驱动的测试方法,我想计算机专业出身的小伙伴应该不会陌生。这里我希望读者对等价类、边界值、错误推测、因果图,判定表驱动,正交试验设计... ...这些概念有一定的了解(知道是什么就行)。简单来说,数据驱动的测试指的是我们的测试输入和测试结果有着一定的关系,不同的输入可能会导致输出结果的不同。例如:测试登录方法,不同的用户名\密码输入后,会显示不一样的错误信息。这里,我不想过多的讨论数据驱动的测试方法应该如何设计相关的测试用例。本文的目的只要是向大家展示xUnit.Net对数据驱动的支持。
xUnit.Net对数据驱动测试方法的支持是通过Theory attribute实现的。你可以用Theory替代Fact来标记你的测试方法,于此同时使用[XXXData]来提供你的输入和输出数据。目前[XXXData] attribute包括[InlineData]和[MemberData]。下面我们会介绍这些 attribute的使用。
查看Theory的源码可以看到,Theory是继承自Fact的。因此,之前提到的DisplayName和Skip也同样适用于Theory。
1 [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 2 [XunitTestCaseDiscoverer("Xunit.Sdk.TheoryDiscoverer", "xunit.execution.{Platform}")] 3 public class TheoryAttribute : FactAttribute 4 { 5 public TheoryAttribute(); 6 }
Theory 和 InlineData 提供了一种简单的数据驱动方式,代码如下:
1 [Theory(DisplayName = "Lesson02.Demo03")] 2 [InlineData(1, 1, 2)] 3 [InlineData(1, 2, 3)] 4 [InlineData(2, 2, 4)] 5 public void Demo03_Theory_Test(int num01, int num02, int result) 6 { 7 Assert.Equal<int>(result, num01 + num02); 8 }
InlineData标签的构造函数接受一个params object[] data类型的参数,值得注意的是InlineData参数的类型和数量应当与测试方法完全匹配。在Test Explorer视图中我们可以看到,该方法相当于三个测试用例,这很好的提高了测试用例的复用率和可维护性:
InlineData已经为我们提供了基本的数据驱动测试的能力,但同时也有几个问题:
面对上述的情况的时候,我们就需要使用MemberData来完成工作。顾名思义,MemberData使用了一个当前类的某个成员来完成数据测试数据的注入,也就是用你可以使用当前测试类的方法,属性,字段进行数据的注入。是不是感觉棒棒哒~~。首先,我们来看一下MemberData的定义:
1 [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] 2 [CLSCompliant(false)] 3 [DataDiscoverer("Xunit.Sdk.MemberDataDiscoverer", "xunit.core")] 4 public sealed class MemberDataAttribute : MemberDataAttributeBase 5 { 6 public MemberDataAttribute(string memberName, params object[] parameters); 7 8 protected override object[] ConvertDataItem(MethodInfo testMethod, object item); 9 }
[MemberData]构造函数接受两个参数:第一,成员名称(即方法,属性或字段的名称)。第二,一个参数列表(只针对方法)。例外,需要注意以下两点:
下面我们来看几个具体的例子:
下面的Code中定义了属性 InputData_Property,并在测试方法上用MemberData标记说明数据源来自对应的属性。
1 #region MemberData InputData_Property 2 public static IEnumerable<object[]> InputData_Property 3 { 4 get 5 { 6 var driverData = new List<object[]>(); 7 driverData.Add(new object[] { 1, 1, 2 }); 8 driverData.Add(new object[] { 1, 2, 3 }); 9 driverData.Add(new object[] { 2, 3, 5 }); 10 driverData.Add(new object[] { 3, 4, 7 }); 11 driverData.Add(new object[] { 4, 5, 9 }); 12 driverData.Add(new object[] { 5, 6, 11 }); 13 return driverData; 14 } 15 } 16 17 [Theory(DisplayName = "Lesson02.Demo04")] 18 [MemberData("InputData_Property")] 19 public void Demo04_Theory_Test(int num01, int num02, int result) 20 { 21 Assert.Equal<int>(result, num01 + num02); 22 } 23 #endregion
在Test Explorer可以看到对应的测试用例有6组:
下面的Code中定义了属性InputData_Method,细心的同学会发现提供数据源的方法中多了一个flag参数。这个参数的值从何而来呢?就是我们之前说的MemberData属性的第二个构造参数(下面代码的21行)。
1 #region MemberData InputData_Method 2 public static IEnumerable<object[]> InputData_Method(string flag) 3 { 4 var driverData = new List<object[]>(); 5 if (flag == "Default") 6 { 7 driverData.Add(new object[] { 1, 1, 2 }); 8 driverData.Add(new object[] { 1, 2, 3 }); 9 driverData.Add(new object[] { 2, 3, 5 }); 10 } 11 else 12 { 13 driverData.Add(new object[] { 3, 4, 7 }); 14 driverData.Add(new object[] { 4, 5, 9 }); 15 driverData.Add(new object[] { 5, 6, 11 }); 16 } 17 return driverData; 18 } 19 20 [Theory(DisplayName = "Lesson02.Demo05")] 21 [MemberData("InputData_Method", "Default")] 22 //[MemberData("InputData_Method", "Other")] 23 public void Demo05_Theory_Test(int num01, int num02, int result) 24 { 25 Assert.Equal<int>(result, num01 + num02); 26 } 27 #endregion MemberData InputData_Method
此时,我们在Test Exporer视图中只能看见三个测试用例,如图所示。这里xUnit.Net为我们提供了根据不同的需要加载不同数据源的可能。例如:例子中的flag参数可以是一个Excel文件名称,参数不同即可读取不同的文件。这里我就不展开讨论了,后续的文章会专门讨论这个问题。
其实,用属性和方法作为数据源,已经可以解决很多问题了。最后,我们来看一下如何使用字段作为数据源实现数据驱动的测试。
首先,我们定义一个新的类型:
1 public class MatrixTheoryData<T1, T2> : TheoryData<T1, T2> 2 { 3 public MatrixTheoryData(IEnumerable<T1> data1, IEnumerable<T2> data2) 4 { 5 Contract.Assert(data1 != null && data1.Any()); 6 Contract.Assert(data2 != null && data2.Any()); 7 8 foreach (T1 t1 in data1) 9 { 10 foreach (T2 t2 in data2) 11 { 12 Add(t1, t2); 13 } 14 } 15 } 16 }
这里用到了TheoryData类,这个类是有xUnit.Net提供。其中T1,T2表示了输入数据的类型。也就是说这种方式是一种类型安全的输入方式(其实,xUnit还提供了1至5个参数的TheoryData泛型)。这里使用输入的两个数据集合做笛卡尔积的结果,来充当数据源。下面看一下使用的代码:
1 #region MemberData InputData_Field 2 public static int[] Numbers = { 5, 6, 7 }; 3 public static string[] Strings = { "Hello", "world!" }; 4 public static MatrixTheoryData<string, int> MatrixData = new MatrixTheoryData<string, int>(Strings, Numbers); 5 6 [Theory(DisplayName = "Lesson02.Demo06")] 7 [MemberData("MatrixData")] 8 public void Demo06_Theory_Test(string x, int y) 9 { 10 Assert.Equal(y, x.Length); 11 } 12 #endregion MemberData InputData_Field
MatrixData字段在构造的时候就会按照规则(使用Numbers,Strings的笛卡尔积)构造对应的数据源。看一下Test Explorer视图,此方法对应了6(3×2 = 6)个用例,用例的参数就是两个数组的笛卡尔积的组合:
本文主要介绍了xUnit.Net的基本使用和针对数据驱动测试的支持。主要包含以下几点:
小北De系列文章:
《[小北De编程手记] : Selenium For C# 教程》
《[小北De编程手记]:C# 进化史》(未完成)
《[小北De编程手记]:玩转 xUnit.Net》(未完成)
Demo地址:https://github.com/DemoCnblogs/xUnit.Net