關於《玩轉 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