有了上一節自定義配置,很多問題都能解決了,但是如果僅僅是為了解決一個簡單問題那么創建一個類顯得有點繁重.其實AutoFixture在創建Fixture對象時有很多方便的Fluent配置,我們這里介紹一些比較常用了.
創建對象是忽略一些屬性
有些時候有這樣的一些業務場景,有些字段是非必填項,但是一旦填寫則必須符合指定規則.這些非必填字段在業務中僅僅當它存在的時候做一些校驗,其它地方並沒有使用到它.這樣在單元測試的時候我們為了效率可以暫時忽略這些字段.在后面集成測試的時候再提供完整數據.
下面看看AutoFixture在生成對象的時候如何顯式地忽略一些字段
之所以要忽略是因為如果不忽略AutoFixture自動為字符串類型生成一個guid字符串,這將會導致驗證失敗.
我們擴展一下Person類,代碼如下
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime BirthDay { get; set; }
public string Mobile { get; set; }
public string IDCardNo { get; set; }
public string Email { get; set; }
}
我們看配置代碼
[Test]
public void FixValueTest()
{
var fix = new Fixture();
var psn = fix.Build<Person>().
Without(a => a.IDCardNo).
Create();
}
這里fix對象使用Build方法,生成一個自定義生成對象,然后會出現很多自定義配置方法,我們使用without方法指示AutoFixture在生成時不生成某一字段,一個without后面還可以再接一個,如果需要忽略其它字段,可以串聯使用多個without.
指定當前時間
在集成測試的時候,有些關於時間的字段都需要是當前時間,這時候可以使用AutoFixture內置的自定義類CurrentDateTimeGenerator來實現
[Test]
public void FixValueTest()
{
var fix = new Fixture();
fix.Customizations.Add(new CurrentDateTimeGenerator());
var psn = fix.Create<Person>();
}
[info]當然以上配置可能是沒有必要的,因為C#很早就支持屬性賦初值了.
UriGenerator
此配置可以讓AutoFixture生成一個Uri
[Test]
public void FixValueTest()
{
var fix = new Fixture();
fix.Customizations.Add(new UriGenerator());
var br = fix.Create<Uri>()# AutoFixture配置二
有了上一節自定義配置,很多問題都能解決了,但是如果僅僅是為了解決一個簡單問題那么創建一個類顯得有點繁重.其實AutoFixture在創建Fixture對象時有很多方便的Fluent配置,我們這里介紹一些比較常用了.
## 創建對象是忽略一些屬性
有些時候有這樣的一些業務場景,有些字段是非必填項,但是一旦填寫則必須符合指定規則.這些非必填字段在業務中僅僅當它存在的時候做一些校驗,其它地方並沒有使用到它.這樣在單元測試的時候我們為了效率可以暫時忽略這些字段.在后面集成測試的時候再提供完整數據.
下面看看AutoFixture在生成對象的時候如何顯式地忽略一些字段
>之所以要忽略是因為如果不忽略AutoFixture自動為字符串類型生成一個guid字符串,這將會導致驗證失敗.
我們擴展一下Person類,代碼如下
```cs
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime BirthDay { get; set; }
public string Mobile { get; set; }
public string IDCardNo { get; set; }
public string Email { get; set; }
}
我們看配置代碼
[Test]
public void FixValueTest()
{
var fix = new Fixture();
var psn = fix.Build<Person>().
Without(a => a.IDCardNo).
Create();
}
這里fix對象使用Build方法,生成一個自定義生成對象,然后會出現很多自定義配置方法,我們使用without方法指示AutoFixture在生成時不生成某一字段,一個without后面還可以再接一個,如果需要忽略其它字段,可以串聯使用多個without.
指定當前時間
在集成測試的時候,有些關於時間的字段都需要是當前時間,這時候可以使用AutoFixture內置的自定義類CurrentDateTimeGenerator來實現
[Test]
public void FixValueTest()
{
var fix = new Fixture();
fix.Customizations.Add(new CurrentDateTimeGenerator());
var psn = fix.Create<Person>();
}
[info]當然以上配置可能是沒有必要的,因為C#很早就支持屬性賦初值了.
UriGenerator
此配置可以讓AutoFixture生成一個Uri
[Test]
public void FixValueTest()
{
var fix = new Fixture();
fix.Customizations.Add(new UriGenerator());
var br = fix.Create<Uri>();
}
MailAddressGenerator
用於生成郵箱地址
[Test]
public void FixValueTest()
{
var fix = new Fixture();
fix.Customizations.Add(new MailAddressGenerator());
var mr = fix.Create<MailAddress>()
}
當然很多時候我們是想要MailAddress里的字符串.這時候使用MailAddress的Address屬性即可.
;
}
## MailAddressGenerator
用於生成郵箱地址
```cs
[Test]
public void FixValueTest()
{
var fix = new Fixture();
fix.Customizations.Add(new MailAddressGenerator());
var mr = fix.Create<MailAddress>()
}
當然很多時候我們是想要MailAddress里的字符串.這時候使用MailAddress的Address屬性即可.
AutoFixture結合DataAnnotations
有些時候有這樣一些場影,我們的實體類中有很多驗證注解,這就對制造出的假數據有很多要求,比如必須符合郵箱號,身份證號,字符串長度必須為特定值,手機號必須為特定長度數字等等.通過前面講到的自定義配置我們可以實現以上功能,但是如果僅僅是為了生成一個指定長度的字符串我們再新建一個配置類實在有點繁瑣,並且這些邏輯也並非都是通用的.更為復雜的是有時候一個特定字段必須符合某一正則規則,如果這個規則非常復雜想要生成滿足它的假數據確實需要花費些心思,這時候如果AutoFixture能自動生成滿足條件的假數據那該有好多,實際上AutoFixture確實可以生成滿足DataAnnotations約束的字段,並且不需要配置
默認就是支持的.
比如現在Person類改為如下:
public class Person{
[StringLength(10)]
public string Name { get; set; }
[Range(18,42)]
public int Age { get; set; }
public DateTime BirthDay { get; set; }
[RegularExpression("\\d{11}")]
public string Mobile { get; set; }
}
AutoFixture會自動生成滿足條件的字段.
AutoFixture 生成符合特定業務的字段.
Attribute自動生成符合注解約束的字段為集成測試提供了很大方便.然而一些功能不論是注解還是AutoFixture內置的配置都無法完成,這時候就需要自定義的配置.
比如說有以下業務場景,有些業務模型帶有開始時間和結束時間,這里就有一個隱性約束就是結束時間必須大於或者等於開始時間,如果不是這樣數據庫中就無法取到值.這個時候就必須使用自定義配置了.
比如有一下模型:
public class CustomDate
{
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
}
[info]這里只是一個普通例子,很多時候業務里面都有這樣的模型,如果要讓結束時間晚於開始時間,我們首先要先確定哪個時開始時間,哪個是結束時間,這里我們使用基於命名約束的方法來確定它們:即開始時間帶有start,結束時間帶有end(當然也可以是其它標識,只要能確定它們即可).當然這並不是一種很好的設計.理想的情況下是對字段進行注解,但是僅僅為了測試而去擴展現有項目的做法也是值得商榷的.
以下為自定義方法
public class DateTimeSpecimenBuilder:ISpecimenBuilder
{
private readonly Random _random = new Random();
private DateTime startDate = DateTime.Now;
public object Create(object request, ISpecimenContext context)
{
var pi = request as PropertyInfo;
if (pi != null && pi.Name.ToLower().Contains("start") &&
(pi.PropertyType == typeof(DateTime) || pi.PropertyType == typeof(DateTime?)))
{
var stDate = context.Create<DateTime>();
startDate =stDate ;
return startDate;
}
if (pi != null && pi.Name.ToLower().Contains("end") &&
(pi.PropertyType == typeof(DateTime) || pi.PropertyType == typeof(DateTime?)))
{
var endDate = startDate.AddDays(_random.Next(1,20));
return endDate;
}
return new NoSpecimen();
}
}
[Test]
public void FixValueTest()
{
var fix = new Fixture();
fix.Customizations.Add(new DateTimeSpecimenBuilder());
var customDate = fix.Create<CustomDate>();
}
簡單梳理一下以上代碼,基本邏輯就是如果傳入字段包含start關鍵特征並且是日期類型,我們就把它的值存起來,當后面遇到包含end特征的屬性時就把剛才存入的值再加上指定天數這樣就能保證enddate大於startdate了.
生成引用類型時指定構造函數
當一個類有多個構造函數時,AutoFixture默認使用參數最少的構造函數來構造一個對象,但是這在有些時候會造成麻煩:一個Bll類可能有多個構造函數,構造函數里傳入的是依賴對象,如果只調用參數最少的構造函數則很多依賴無法傳入,這樣如果使用到了依賴對象就會報Null引用異常.這個時候我們就需要顯式的指定調用哪一個構造函數.
我們仍然通過示例來講解.
我們把Person類改成如下:
public class Person
{
public Person(string name)
{
Name = name;
}
public Person(string name,int age)
{
Age = age;
Name = name;
}
[StringLength(10)]
public string Name { get; set; }
[Range(18,42)]
public int Age { get; set; }
public DateTime BirthDay { get; set; }
[RegularExpression("\\d{11}")]
public string Mobile { get; set; }
public string IDCardNo { get; set; }
與之前相比,這個類多了兩個有參構造函數.
注意,即使類型不包含無參構造函數,AutoFixture依然能夠創建它,只是使用哪個構造函數是不確定的.
要實現讓AutoFixture選擇我們想要的構造函數,我們創建一個實現了IMethodQuery的類型,然后添加到配置里.
這個類代碼如下
public class MyMethodSelector : IMethodQuery
{
private readonly Type[] _types;
public MyMethodSelector(params Type[] type)
{
_types = type;
}
public IEnumerable<IMethod> SelectMethods(Type type)
{
if (type == null)
{
throw new ArgumentNullException();
}
var constructors = type
.GetConstructors().Where(a => a.GetParameters().Select(t => t.ParameterType).SequenceEqual(_types))
.Select(a => new ConstructorMethod(a));
return constructors;
}
}
我們來分析一下這段代碼,首先我們在構造函數里傳入type 數組,這里的type為構造函數參數的類型.SelectMethods
為接口提供的方法,這個方法接收一個type類型作為參數,這個type為操作的對象的類型.然后我們使用GetConstructors方法獲取它所有的構造函數.然后通過Where過濾符合條件的(條件是參數的類型和構造函數傳入的類型一樣).然后我們使用過濾后的Constructorinfo來創建一個ConstructorMethod,ConstructorMethod為AutoFixture提供的一個類型.
下面是測試代碼
var fix = new Fixture();
fix.Customize(new ConstructorCustomization(typeof(Person),
new MyMethodSelector(typeof(string), typeof(int))));
var psn = fix.Create<Person>();
這里我們給MyMethodSelector傳入了兩個類型,分別是string類型和int類型,以期望AutoFixture調用包含string和int參數的構造方法.我們啟用調試模式,就可以看到Person的第二個構造函數被調用了.
看到這里,很多人可能會感覺有些厭煩,感覺這樣做還不如直接New一個對象,在new的時候顯式調用特定的構造函數就不會有這么麻煩了.關於直接new對象的缺點前面也說過,如果Bll層有變動,則需要顯式修改測試方法,不利於維護,並且這個方法是可以通用的.一旦創建好之后以后遇到這樣的業務就可以直接調用了.