1. 程式人生 > >.net測試篇之測試神器Autofixture在幾個複雜場景下的使用示例以及與Moq結合

.net測試篇之測試神器Autofixture在幾個複雜場景下的使用示例以及與Moq結合

系列目錄

為String指定一個值.

在第三節裡我們講了如何使用自定義配置加上一個自定義演算法生成一個自定義字串,然而有些時候我們僅僅是需要某個欄位是有意義的,這個時候隨便生成的字串也滿足不了我們的需求.在一些簡單場景下,我們可以顯式的給一個欄位指定一個值.
看以下程式碼

        [Test]
        public void FixValueTest()
        {
            var fix = new Fixture();
           var psn= fix.Build<Person>().With(a => a.Name,"xiaodu").Create();
        }

這裡的Build方法返回一個IcustomizationComposer物件,這個物件有很多方法,其中一個為with,可以指定一個要賦值的欄位,然後給它指定一個值.這樣生成出來的物件的指定欄位的值就是我們確切想要的了.

兩個屬性有一定關係

前面我們講到過一個很普遍的場景,與時間有關的業務往往要求結束時間大於開始時間,我們前面講了一種自定義的處理方法.這種方法比較完美的實現是結合自定義Attribute來實現,然而為了實現測試去擴充套件現有專案程式碼有些不妥,我們採用的是基於特徵的辦法(即預先約定開始時間帶欄位名帶有start,結束欄位名帶有end).這樣也會帶來問題,專案中的過多自定義慣例會給後來維護者帶來不小的壓力.並且它只解決了一個問題,實際業務中還可能有其它的關係:比如可能是一個int欄位的值必須要大於另一個int欄位值,使用者的全名是由姓和名結合成的等等.並且最致命的一個問題是我們如果要給一個現有的專案寫單元測試,現有專案早於我們的規則之前出現,它的欄位已經確定了,這時候我們不太可能去修改業務欄位去適應單元測試.這是一個不小的成本!

下面講一下如何像上面一樣通過行內配置解決這一問題.

我們看以下程式碼

    [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在生成物件的時候不要按照預設邏輯生成這兩個欄位,然後執行一個Do方法,這個Do方法接受一個Action型別委託,T即我們要Build的物件,我們通過這個Do方法來執行一些賦值操作.

注意Without是必須的,不然AutoFixture在生成物件的時候會覆蓋Do方法,仍然執行它內部的生成邏輯.

AutoFixture會忽略Without裡面指定的引數,其它沒有忽略的按它內建的邏輯生成.

集合中元素之間有關係.

有一個這樣的業務場,大學新生入學時,會給同學們生成一個惟一編號,這個編號一般是根據入學時間+院系編碼+專業編碼+自增欄位生成的.假設我們要對學生管理系統進行測試,現在要模擬一批學生,我們可以用AutoFixture生成一個學生集合,然而學生的編碼不是任意數字,必須是指定規則的一串數字.這裡我們仍然可以通過Do函式來解決這個問題.

我們把Person類當作學生類

        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為固定值,後面四位為增加值.我們通過對數字格式化生成了15滿足以上規則的學生編號.

AutoFixture結合AutoData註解.

在本章剛開始的時候我們就介紹了使AutoFixture與Nunit相結合,為Nunit提供測試資料.當時講碰到一個問題就是它生成集合物件時預設一個包含三個元素的集合.並且也無法在AutoData註解裡改變這個預設.這裡我們講下如何結合後來的章節的知識實現可以在註解中自定義生成元素集合的個數.這樣,如果我們只是需要資料,就不需要每都次建立一個fix的物件然後再配置了.

我們要實現以上只需要建立一個類繼承AutoData就行了.下面看看這個類如何建立的.

  public class CustomAutoDataAttribute : AutoDataAttribute
    {
        public CustomAutoDataAttribute() : base(() => new Fixture(){RepeatCount=10})
        {
           
        }
    }

我們前面的章節介紹過,可以在建立fixture時給Repeatcount引數指定值,這樣就可以生成指定數量元素的集合了.

測試類新增上這個CustomAutoDataAttribute註解就可以生成包含有10個元素的集合啦.

        [Test]
        [CustomAutoData]
        public void FixValueTest(IEnumerable<string> str)
        {
            Assert.True(str.Count() == 10);
        }

這樣雖然好了一些,但是仍然不夠靈活,要是能做到可以手動指定每次生成的個數就好了.
這個其實就很簡了.

public class CustomAutoDataAttribute : AutoDataAttribute
    {
        public CustomAutoDataAttribute(int count=4) : base(() => new Fixture(){RepeatCount=count})
        {

        }
    }

我們給建構函式增加一個count引數就ok啦.

我們再來看一個更復雜一點的,就是上一節剛講到過的一個日期必須晚於另一個日期的配置,如何做成是AutoData的配置.
由於DateTimeSpecimenBuilder是一個ISpecimenBuilder型別物件,它是通過fix.Customizations.add來新增的.我們再看上面的示例,我們的功能實際上通過給base的建構函式傳入一個Func委託來完成的.而fix.Customizations.add方法返回的是void型別,因此無法在這裡使用了.這裡的配置更為複雜一些.

 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是我們上節建立的.它的程式碼如下

 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)
        {
           
        }

通過以上講解,應該基本的把自定義配置轉成autodata配置的問題都能搞定了.

AutoFixture結合Moq

通過前面介紹我們可能已經發現AutoFixture在生成測試資料方面非常強大.然而它有一個不足:那就是它僅僅是在執行的時候通反射獲取型別資訊,然後根據一定演算法為型別的欄位進行賦值,因此如果一個類的建構函式裡都是介面它就無能為力了.我們知道Moq則可以在編譯階段為介面生成代理型別.如果能將兩者結合起來就完美了.AutoFixture可能聽到了我們的呼聲,特為AutoFixture製作了一個結合Moq的擴充套件.

為什要把二者結合起來

前面說過,AutoFixture結合Moq主要是為擴充套件

比如說有以下這樣一個型別

 public class XXXBll{
     public XXXBll(Interface1 x1,Interface1 x2,Interface1 x3,Interface1 x4,Interface1 x5,Interface1 x6)

 }

以上一個Bll類依賴6個注入物件,實際過程中可能有的bll遠比這要多,可能是十幾個甚至幾十個.

我們通過New建立這個型別他帶來維護上的麻煩,前面已經說過,如果某個依賴物件移除了,則測試程式碼也要改.這倒罷了,麻煩一點就算了,這裡面還可能有一個致命的問題,那就是如果這個Bll還依賴於一個物件而不是介面,這樣就更麻煩了.

 public class XXXBll{
     public XXXBll(Interface1 x1,Interface1 x2,Interface1 x3,Interface1 x4,Interface1 x5,Interface1 x6,SMSServicexxx)
     private SMSService service;

 }

比如說我們業務層還依賴於一個簡訊服務,這個服務是第三方提供的,它只有一個類,並沒有介面.這便是AutoFixture與Moq結合的理想場景,AutoFixture建立物件,遇到介面由moq建立.此時可維護性與可讀性都大大提高.

下面我們介紹如何結合二者.

首先,在Nuget包管理器裡面輸入autofixture automoq 進行搜尋

其中紅框標識的包即為我們想要下載的包.實際專案中,只需要安裝下面的AutoFixture和這個包就行了,因為它依賴於Moq會自動下載Moq.

Person類現在改成如下這樣


   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結合的工作是由AutoFixture來完成的,我們並不需要特別複雜的配置即可實現非常好的擴充套件性和可維護性.這裡的關鍵程式碼就是在Customize方法裡傳入一個AutoMoqCustomization物件,這個物件是由AutoFixture提供的,並不需要我們自己建立.

我們啟用除錯模式檢視以下生成的物件

可以看到前三個介面實體是由Moq生成的,而最後一個SMSService則是由AutoFixture生成的.這樣就完美解決了我們的問題.

新問題解決

這個做又引入了一個新的問題:我們知道Moq出現的型別是一個預設實現,沒有任何功能,它會把預設值賦值給值型別,把null賦值給引用型別.比如以上IMember裡的IsMember只是會返回預設值false,而實際測試中我們要根據使用者名稱型別使用者是否是會員,有的是,有的不是,如果全返回false顯然對單元測試不利,更為要命的是很多方法如果是null就丟擲異常或者返回了,這就會導致業務方法很快返回,很多業務程式碼會覆蓋不到.

我們知道.Moq可以通過配置讓moq的屬性或者方法返回指定值.然而看我們以上測試程式碼,沒有一行跟Moq有關.這該怎麼辦呢.

我們仍然通過示例講解


        [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方法,後面建立介面的模擬實現的時候會自動呼叫這個凍結的物件.

凍結的這個物件是一個Moq物件,我樣我們就可以像以前在Moq章節裡講到過的方法來配置它