Nunit里提供了豐富的數據測試功能,雖然Xunit里提供的比較少,但是也能滿足很多場景下使用了,如果數據場景非常復雜,Nunit和Xunit都是無法勝任的,有不少測試者選擇自己編寫一個數據提供程序,但是更建議使用AutoFixture框架,一是因為自己工作中寫的往往只是為了解決某個或者部分問題,只能隨着業務邏輯的擴展才能不斷的健壯起來,二是這樣的框架往往缺少良好文檔,主要由核心開發者口口相傳,這就導致后來者遇到不明白了功能就去問核心開發者,影響這些開發者的其它工作.
下面介紹一下Xunit里的數據提供方式.
InlineData
InlineData相當於Nunit里的TestCase,用注解的方式給測試方法提供數據.
我們通過以下代碼片段了解它的基本用法
[Theory]
[InlineData(1, 2)]
[InlineData(5, 9)]
public void Test1(int x,int y)
{
int result = x + y;
Assert.Equal(x + y, result);
}
以上方法與普通測試方法相比最大的區別是它使用的是Theory注解,而不是fact注解.使用Theory注解的方法必須提供相應的參數,否則會報編譯錯誤.
以上測試我們提供了兩組InlineData,這樣在測試運行的時候測試方法就會根據這些數據生成兩個方法實例.同Nunit里的表現行為相似.
MemberData
MemberData顧名思義,就是成員數據,它類似於Nunit里的TestCaseSource
但是不同的是Xunit的MemberData的數據提供者必須是當前測試類的成員,測試數據提供者和測試方法耦合在一塊可能不是太好的設計,如果需要大量測試數據,建議使用AutoFixture.
數據提供者之屬性提供數據
通過屬性提供測試數據適應於一些比較簡單的場景,這些數據是簡單的,確定的.
下面看一個示例
[Theory]
[MemberData(nameof(UnitTest1.ProvideData))]
public void Test1(int x,int y)
{
int result = x + y;
Assert.Equal(x + y, result);
}
public static IEnumerable<object[]> ProvideData
{
get
{
yield return new object[] { 3, 4 };
yield return new object[] { 5, 9 };
yield return new object[] { 11, 13 };
}
}
以上代碼中,測試方法和數據提供者必須位於同一個類中,並且數據提供者必須是一個公開的,靜態的屬性.並且它的集合元素類型必須是Object類型.像以上Test1方法雖然需要的是int類型參數,但是提供者類型也必須是object類型,而不能是具體類型.
以上數據提供屬性一共yield了三組數據,因此測試方法會生成三個測試實例.
數據提供者之方法提供數據
[Theory]
[MemberData(nameof(UnitTest1.ProvideData))]
public void Test1(int x,int y)
{
int result = x + y;
Assert.Equal(x + y, result);
}
public static IEnumerable<object[]> ProvideData()
{
yield return new object[]{3,4 };
yield return new object[] {5, 9};
yield return new object[] { 11, 13 };
}
你可能會感覺以上方法和屬性並沒太大的區別,其實方法的功能更為強大,因為屬性無法動態指定參數,而方法可以,我們可以指定方法接收動態運行時需要的參數,然后在MemberData的構造函數里傳入參數來動態獲取數據.
數據提供者之成員提供數據
成員提供數據可以把外部對象作為本類成員,然后給測試方法提供數據.外部對象須繼承自TheoryData.
我們定義一個MyDataprovider
public class MyDataprovider<TData1,TData2>:TheoryData<TData1,TData2>
{
public MyDataprovider(IEnumerable<TData1> dataSource1,IEnumerable<TData2> datasource2)
{
if (dataSource1 == null || datasource2 == null || !dataSource1.Any() || !datasource2.Any())
throw new Exception("集合不為能空或者null");
foreach (TData1 data1 in dataSource1)
{
foreach (TData2 data2 in datasource2)
{
Add(data1, data2);
}
}
}
}
我們再看測試類
public class UnitTest1
{
public static MyDataprovider<int, int> myprovider =
new MyDataprovider<int, int>(new[] {3, 4, 5}, new[] {6, 7, 8});
[Theory]
[MemberData(nameof(UnitTest1.myprovider))]
public void Test1(int x,int y)
{
int result = x + y;
Assert.Equal(x + y, result);
}
}
我們在new MyDataprovider的時候通過構造函數傳入兩個集合,MyDataprovider繼承了TheoryData的Add方法,把數據添加到theorydata中.
以上方法實際上生成了一個笛卡爾集{{3,6},{3,7},{3,8},{4,6},{4,7},{4,8},{5,6},{5,7},{5,8}}類似於Nunit里的values注解不加sequential,這個行為很多時候可能並不是我們想要的,我們想要的可能是{{3,6},{4,7},{5,8}}這樣的組合,這其實是可以在MyDataprovider里自定義的.
我們把MyDataprovider改為如下就可以了
public class MyDataprovider<TData1,TData2>:TheoryData<TData1,TData2>
{
public MyDataprovider(IEnumerable<TData1> dataSource1,IEnumerable<TData2> datasource2)
{
if (dataSource1 == null || datasource2 == null || !dataSource1.Any() || !datasource2.Any())
throw new Exception("集合不為能空或者null");
var count1 = dataSource1.Count();
var count2 = datasource2.Count();
if (count1 != count2) throw new ArgumentException("兩個集合長度必須相等");
for (int i = 0; i < count1; i++)
{
Add(dataSource1.ElementAt(i), datasource2.ElementAt(i));
}
}
}
這樣雖然可以把數據提供者轉移到外部了,然而去把簡單的問題搞的相當復雜!
數據提供者之類數據提供者
前面介紹的數據提供者除了InlineData比較常用外,其它幾個都不是很實用,因為數據和測試方法混合在一個類中,違反了職責單一的原則,最后一個看似比較好的解開了耦合,實際上卻帶來了更高的復雜度.這里介紹ClassDataAttribute,類數據提供者.
類數據提供者需要實現IEnumerable<Object[]>泛型接口,Xunit會自動的調用其GetEnumerator方法來遍歷數據然后提供給測試類.
我們看以下數據提供類
public class MyDataClassProvider:IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] {3, 4};
yield return new object[] {5, 9};
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
以上類型的GetEnumerator繼承自接口,我們這里只提供了一些簡單數據,當然帶可以編寫更為復雜的數據提供邏輯,比如從數據庫里遍歷,然后轉化為可遍歷集合.
下面再看看它是如何被使用的.
[Theory]
[ClassData(typeof(MyDataClassProvider))]
public void Test1(int x,int y)
{
var result = x + y;
Assert.Equal(x + y, result);
}
這里使用ClassData注解,傳入一個type類型.運行的時候Xunit便可以給測試方法提供測試數據了