[小北De编程手记] : Lesson 02 玩转 xUnit.Net 之 基本UnitTest & 数据驱动
关于《玩转 xUnit.Net》系列文章,我想跟大家分享的不是简单的运行一下测试用例或是介绍一下标签怎么使用(这样的文章网上很多)。上一篇《Lesson 01 玩转 xUnit.Net 之 概述》介绍xUnit.Net的一些基本概念。从这一篇开始我将会为大家逐一展示xUnit.Net的强大之处。还是先看一下本文要讨论的内容:
- xUnit.Net 单元测试用例的类型
- 简单测试用例 & Fact 标签
- 数据驱动的用例 & Theory 标签
(一)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:表示测试结果永远成立的那些Unit Test,他们的输入条件不变。
- Theory:表示测试是针对某一组数据的(即需要数据驱动的Unit Test<data-driven test>)。
(二)简单的测试用例 & Fact 标签
首先,我们来看一下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还提供了两个属性。
- DisplayName : 用来设置Test Case 显示名称(如果是自定义的Runner,也可以通过该属性获取Test Case的名称)。
- Skip:如果设置了该属性,相当于Ignore了对应的测试用例,该用例将不会被运行。
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没有被执行。
(三)数据驱动的用例 & Theory 标签
@基本概念
关于数据驱动的测试方法,我想计算机专业出身的小伙伴应该不会陌生。这里我希望读者对等价类、边界值、错误推测、因果图,判定表驱动,正交试验设计... ...这些概念有一定的了解(知道是什么就行)。简单来说,数据驱动的测试指的是我们的测试输入和测试结果有着一定的关系,不同的输入可能会导致输出结果的不同。例如:测试登录方法,不同的用户名\密码输入后,会显示不一样的错误信息。这里,我不想过多的讨论数据驱动的测试方法应该如何设计相关的测试用例。本文的目的只要是向大家展示xUnit.Net对数据驱动的支持。
xUnit.Net对数据驱动测试方法的支持是通过Theory attribute实现的。你可以用Theory替代Fact来标记你的测试方法,于此同时使用[XXXData]来提供你的输入和输出数据。目前[XXXData] attribute包括[InlineData]和[MemberData]。下面我们会介绍这些 attribute的使用。
@Theory简介
查看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 }
@InlineData
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视图中我们可以看到,该方法相当于三个测试用例,这很好的提高了测试用例的复用率和可维护性:
@MemberData
InlineData已经为我们提供了基本的数据驱动测试的能力,但同时也有几个问题:
- 当测试样本很多时(尤其是在划分出的等价类数量很多,或是想做大样本测试的情况下),就会导致测试用了的InlineData变得非常长。
- 测试数据是从外部导入(例如:Excel,数据库,文本文件... ...),而不是硬编码。
面对上述的情况的时候,我们就需要使用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]构造函数接受两个参数:第一,成员名称(即方法,属性或字段的名称)。第二,一个参数列表(只针对方法)。例外,需要注意以下两点:
- 为MemberData提供数据源的方法,属性或字段都必须是静态的。
- 成员返回的测试数据类型应该是IEnumerable<object[]>。
下面我们来看几个具体的例子:
示例一: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组:
示例二:MemberData & 方法
下面的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文件名称,参数不同即可读取不同的文件。这里我就不展开讨论了,后续的文章会专门讨论这个问题。
示例三:MemberData & 字段
其实,用属性和方法作为数据源,已经可以解决很多问题了。最后,我们来看一下如何使用字段作为数据源实现数据驱动的测试。
首先,我们定义一个新的类型:
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的基本使用和针对数据驱动测试的支持。主要包含以下几点:
- xUnit.Net基本使用:Fact简介。
- 数据驱动测试基本概念。
- xUnit.Net 针对数据驱动测试的支持。
- 描述了以属性,方法,字段作为数据源的异同。
小北De系列文章:
《[小北De编程手记] : Selenium For C# 教程》
《[小北De编程手记]:C# 进化史》(未完成)
《[小北De编程手记]:玩转 xUnit.Net》(未完成)
Demo地址:https://github.com/DemoCnblogs/xUnit.Net