系列目錄 <https://www.cnblogs.com/tylerzhou/p/11204826.html>
為String指定一個值.
在第三節(jié)里我們講了如何使用自定義配置加上一個自定義算法生成一個自定義字符串,然而有些時候我們僅僅是需要某個字段是有意義的,這個時候隨便生成的字符串也滿足不了我們的需求.在一些簡單場景下,我們可以顯式的給一個字段指定一個值.
看以下代碼
[Test] public void FixValueTest() { var fix = new Fixture(); var psn=
fix.Build<Person>().With(a => a.Name,"xiaodu").Create(); }
這里的Build方法返回一個IcustomizationComposer對象,這個對象有很多方法,其中一個為with,可以指定一個要賦值的字段,然后給它指定一個值.這樣生成出來的對象的指定字段的值就是我們確切想要的了.
兩個屬性有一定關(guān)系
前面我們講到過一個很普遍的場景,與時間有關(guān)的業(yè)務(wù)往往要求結(jié)束時間大于開始時間,我們前面講了一種自定義的處理方法.這種方法比較完美的實現(xiàn)是結(jié)合自定義Attribute來實現(xiàn),然而為了實現(xiàn)測試去擴(kuò)展現(xiàn)有項目代碼有些不妥,我們采用的是基于特征的辦法(即預(yù)先約定開始時間帶字段名帶有start,結(jié)束字段名帶有end).這樣也會帶來問題,項目中的過多自定義慣例會給后來維護(hù)者帶來不小的壓力.并且它只解決了一個問題,實際業(yè)務(wù)中還可能有其它的關(guān)系:比如可能是一個int字段的值必須要大于另一個int字段值,用戶的全名是由姓和名結(jié)合成的等等.并且最致命的一個問題是我們?nèi)绻o一個現(xiàn)有的項目寫單元測試,現(xiàn)有項目早于我們的規(guī)則之前出現(xiàn),它的字段已經(jīng)確定了,這時候我們不太可能去修改業(yè)務(wù)字段去適應(yīng)單元測試.這是一個不小的成本!
下面講一下如何像上面一樣通過行內(nèi)配置解決這一問題.
我們看以下代碼
[Test] public void FixValueTest() { var fix = new Fixture(); var psn =
fix.Build<CustomDate>().Without(a => a.StartTime).Without(a=>a.EndTime).Do(a =>
{ var dt = DateTime.Now; a.StartTime = dt; a.EndTime = dt.AddDays(3);
}).Create(); }
這里使用Without方法顯式指示AutoFixture在生成對象的時候不要按照默認(rèn)邏輯生成這兩個字段,然后執(zhí)行一個Do方法,這個Do方法接受一個Action
類型委托,T即我們要Build的對象,我們通過這個Do方法來執(zhí)行一些賦值操作.
注意Without是必須的,不然AutoFixture在生成對象的時候會覆蓋Do方法,仍然執(zhí)行它內(nèi)部的生成邏輯.
AutoFixture會忽略Without里面指定的參數(shù),其它沒有忽略的按它內(nèi)置的邏輯生成.
集合中元素之間有關(guān)系.
有一個這樣的業(yè)務(wù)場,大學(xué)新生入學(xué)時,會給同學(xué)們生成一個惟一編號,這個編號一般是根據(jù)入學(xué)時間+院系編碼+專業(yè)編碼+自增字段生成的.假設(shè)我們要對學(xué)生管理系統(tǒng)進(jìn)行測試,現(xiàn)在要模擬一批學(xué)生,我們可以用AutoFixture生成一個學(xué)生集合,然而學(xué)生的編碼不是任意數(shù)字,必須是指定規(guī)則的一串?dāng)?shù)字.這里我們?nèi)匀豢梢酝ㄟ^Do函數(shù)來解決這個問題.
我們把Person類當(dāng)作學(xué)生類
public string Code { get; set; } [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; }
測試代碼如下
[Test] public void FixValueTest() { var fix = new Fixture(); int inc = 1; var
students = fix.Build<Person>().Without(a => a.Code).Do(a => { string code =
$"{inc++:20070102000#}"; a.Code = code; }).CreateMany(15);
以上測試代碼中,20070102為固定值,后面四位為增加值.我們通過對數(shù)字格式化生成了15滿足以上規(guī)則的學(xué)生編號.
AutoFixture結(jié)合AutoData注解.
在本章剛開始的時候我們就介紹了使AutoFixture與Nunit相結(jié)合,為Nunit提供測試數(shù)據(jù).當(dāng)時講碰到一個問題就是它生成集合對象時默認(rèn)一個包含三個元素的集合.并且也無法在AutoData注解里改變這個默認(rèn).這里我們講下如何結(jié)合后來的章節(jié)的知識實現(xiàn)可以在注解中自定義生成元素集合的個數(shù).這樣,如果我們只是需要數(shù)據(jù),就不需要每都次創(chuàng)建一個fix的對象然后再配置了.
我們要實現(xiàn)以上只需要創(chuàng)建一個類繼承AutoData就行了.下面看看這個類如何創(chuàng)建的.
public class CustomAutoDataAttribute : AutoDataAttribute { public
CustomAutoDataAttribute() : base(() => new Fixture(){RepeatCount=10}) { } }
我們前面的章節(jié)介紹過,可以在創(chuàng)建fixture時給Repeatcount參數(shù)指定值,這樣就可以生成指定數(shù)量元素的集合了.
測試類添加上這個CustomAutoDataAttribute注解就可以生成包含有10個元素的集合啦.
[Test] [CustomAutoData] public void FixValueTest(IEnumerable<string> str) {
Assert.True(str.Count() == 10); }
這樣雖然好了一些,但是仍然不夠靈活,要是能做到可以手動指定每次生成的個數(shù)就好了.
這個其實就很簡了.
public class CustomAutoDataAttribute : AutoDataAttribute { public
CustomAutoDataAttribute(int count=4) : base(() => new
Fixture(){RepeatCount=count}) { } }
我們給構(gòu)造函數(shù)增加一個count參數(shù)就ok啦.
我們再來看一個更復(fù)雜一點的,就是上一節(jié)剛講到過的一個日期必須晚于另一個日期的配置,如何做成是AutoData的配置.
由于DateTimeSpecimenBuilder是一個ISpecimenBuilder類型對象,它是通過fix.Customizations.add來添加的.我們再看上面的示例,我們的功能實際上通過給base的構(gòu)造函數(shù)傳入一個Func
委托來完成的.而fix.Customizations.add方法返回的是void類型,因此無法在這里使用了.這里的配置更為復(fù)雜一些.
public class CustomAutoDataAttribute : AutoDataAttribute { public
CustomAutoDataAttribute() : base(() => new Fixture().Customize(new
ValidDateRangeCustomization())) { } }
其中使用到的ValidDateRangeCustomization類定義如下
public class ValidDateRangeCustomization : ICustomization { public void
Customize(IFixture fixture) { fixture.Customizations.Add(new
DateTimeSpecimenBuilder()); } }
我們在這里添加DateTimeSpecimenBuilder這個builder是我們上節(jié)創(chuàng)建的.它的代碼如下
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] [CustomAutoData] public void FixValueTest(CustomDate custom) { }
通過以上講解,應(yīng)該基本的把自定義配置轉(zhuǎn)成autodata配置的問題都能搞定了.
AutoFixture結(jié)合Moq
通過前面介紹我們可能已經(jīng)發(fā)現(xiàn)AutoFixture在生成測試數(shù)據(jù)方面非常強(qiáng)大.然而它有一個不足:那就是它僅僅是在運行的時候通反射獲取類型信息,然后根據(jù)一定算法為類型的字段進(jìn)行賦值,因此如果一個類的構(gòu)造函數(shù)里都是接口它就無能為力了.我們知道Moq則可以在編譯階段為接口生成代理類型.如果能將兩者結(jié)合起來就完美了.AutoFixture可能聽到了我們的呼聲,特為AutoFixture制作了一個結(jié)合Moq的擴(kuò)展.
為什要把二者結(jié)合起來
前面說過,AutoFixture結(jié)合Moq主要是為擴(kuò)展
比如說有以下這樣一個類型
public class XXXBll{ public XXXBll(Interface1 x1,Interface1 x2,Interface1
x3,Interface1 x4,Interface1 x5,Interface1 x6) }
以上一個Bll類依賴6個注入對象,實際過程中可能有的bll遠(yuǎn)比這要多,可能是十幾個甚至幾十個.
我們通過New創(chuàng)建這個類型他帶來維護(hù)上的麻煩,前面已經(jīng)說過,如果某個依賴對象移除了,則測試代碼也要改.這倒罷了,麻煩一點就算了,這里面還可能有一個致命的問題,那就是如果這個Bll還依賴于一個對象而不是接口,這樣就更麻煩了.
public class XXXBll{ public XXXBll(Interface1 x1,Interface1 x2,Interface1
x3,Interface1 x4,Interface1 x5,Interface1 x6,SMSServicexxx) private SMSService
service; }
比如說我們業(yè)務(wù)層還依賴于一個短信服務(wù),這個服務(wù)是第三方提供的,它只有一個類,并沒有接口.這便是AutoFixture與Moq結(jié)合的理想場景,AutoFixture創(chuàng)建對象,遇到接口由moq創(chuàng)建.此時可維護(hù)性與可讀性都大大提高.
下面我們介紹如何結(jié)合二者.
首先,在Nuget包管理器里面輸入autofixture automoq 進(jìn)行搜索
其中紅框標(biāo)識的包即為我們想要下載的包.實際項目中,只需要安裝下面的AutoFixture和這個包就行了,因為它依賴于Moq會自動下載Moq.
Person類現(xiàn)在改成如下這樣
public interface IPerson { } public interface IMember { bool IsMember(string
name);} public interface IDoWork { } public class SMSService { } public class
Person { private readonly IPerson _person; private readonly IMember _member;
private readonly IDoWork _doWork; private readonly SMSService _service; public
Person(IPerson person,IMember member,IDoWork doWork,SMSService service)) {
_person = person; _member = member; _doWork = doWork; _service = service; }
public bool isMember(string name) { if (string.IsNullOrEmpty(name)) return
false; return _member.IsMember(name); }
測試代碼如下
[Test] public void FixValueTest() { var fix = new Fixture();
fix.Customize(new AutoMoqCustomization()); var psn = fix.Create<Person>(); }
AutoFixture與Moq結(jié)合的工作是由AutoFixture來完成的,我們并不需要特別復(fù)雜的配置即可實現(xiàn)非常好的擴(kuò)展性和可維護(hù)性.這里的關(guān)鍵代碼就是在Customize方法里傳入一個AutoMoqCustomization對象,這個對象是由AutoFixture提供的,并不需要我們自己創(chuàng)建.
我們啟用調(diào)試模式查看以下生成的對象
可以看到前三個接口實體是由Moq生成的,而最后一個SMSService則是由AutoFixture生成的.這樣就完美解決了我們的問題.
新問題解決
這個做又引入了一個新的問題:我們知道Moq出現(xiàn)的類型是一個默認(rèn)實現(xiàn),沒有任何功能,它會把默認(rèn)值賦值給值類型,把null賦值給引用類型.比如以上IMember里的IsMember只是會返回默認(rèn)值false,而實際測試中我們要根據(jù)用戶名類型用戶是否是會員,有的是,有的不是,如果全返回false顯然對單元測試不利,更為要命的是很多方法如果是null就拋出異常或者返回了,這就會導(dǎo)致業(yè)務(wù)方法很快返回,很多業(yè)務(wù)代碼會覆蓋不到.
我們知道.Moq可以通過配置讓moq的屬性或者方法返回指定值.然而看我們以上測試代碼,沒有一行跟Moq有關(guān).這該怎么辦呢.
我們?nèi)匀煌ㄟ^示例講解
[Test] public void FixValueTest() { var fix = new Fixture(); var member =
fix.Freeze<Mock<IMember>>(); member.Setup(a => a.IsMember(It.Is<string>(t =>
t.Contains("vip")))).Returns(true); fix.Customize(new AutoMoqCustomization());
var psn = fix.Create<Person>(); Assert.True(psn.isMember("vipxiaoming")); }
與前面相比,我們這里使用了fix對象的Freeze方法,后面創(chuàng)建接口的模擬實現(xiàn)的時候會自動調(diào)用這個凍結(jié)的對象.
凍結(jié)的這個對象是一個Moq對象,我樣我們就可以像以前在Moq章節(jié)里講到過的方法來配置它了.
熱門工具 換一換