1. 程式人生 > >.net測試篇之測試神器Autofixture Generator使用與自定義builder

.net測試篇之測試神器Autofixture Generator使用與自定義builder

有了上一節自定義配置,很多問題都能解決了,但是如果僅僅是為了解決一個簡單問題那麼建立一個類顯得有點繁重.其實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
```cs
[Test]
public void FixValueTest()
{
var fix = new Fixture();
fix.Customizations.Add(new UriGenerator());
var br = fix.Create()# AutoFixture配置二
有了上一節自定義配置,很多問題都能解決了,但是如果僅僅是為了解決一個簡單問題那麼建立一個類顯得有點繁重.其實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>();
        }

MailAddressGenerator

用於生成郵箱地址

 [Test]
        public void FixValueTest()
        {
            var fix = new Fixture();
            fix.Customizations.Add(new MailAddressGenerator());
            var mr = fix.Create<MailAddress>()
        }

當然很多時候我們是想要MailAddress裡的字串.這時候使用MailAddress的Address屬性即可.

;
}
```

MailAddressGenerator

用於生成郵箱地址

 [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層有變動,則需要顯式修改測試方法,不利於維護,並且這個方法是可以通用的.一旦建立好之後以後遇到這樣的業務就可以直接呼叫