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

系列目錄html

爲String指定一個值.

在第三節裏咱們講了如何使用自定義配置加上一個自定義算法生成一個自定義字符串,然而有些時候咱們僅僅是須要某個字段是有意義的,這個時候隨便生成的字符串也知足不了咱們的需求.在一些簡單場景下,咱們能夠顯式的給一個字段指定一個值. 看如下代碼c++

[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字段值,用戶的全名是由姓和名結合成的等等.而且最致命的一個問題是咱們若是要給一個現有的項目寫單元測試,現有項目早於咱們的規則以前出現,它的字段已經肯定了,這時候咱們不太可能去修改業務字段去適應單元測試.這是一個不小的成本!dom

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

咱們看如下代碼單元測試

[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>類型委託,T即咱們要Build的對象,咱們經過這個Do方法來執行一些賦值操做.測試

注意Without是必須的,否則AutoFixture在生成對象的時候會覆蓋Do方法,仍然執行它內部的生成邏輯.ui

AutoFixture會忽略Without裏面指定的參數,其它沒有忽略的按它內置的邏輯生成.編碼

集合中元素之間有關係.

有一個這樣的業務場,大學新生入學時,會給同窗們生成一個唯一編號,這個編號通常是根據入學時間+院系編碼+專業編碼+自增字段生成的.假設咱們要對學生管理系統進行測試,如今要模擬一批學生,咱們能夠用AutoFixture生成一個學生集合,然而學生的編碼不是任意數字,必須是指定規則的一串數字.這裏咱們仍然能夠經過Do函數來解決這個問題.spa

咱們把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<IFixture>委託來完成的.而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 進行搜索

Avatar

其中紅框標識的包即爲咱們想要下載的包.實際項目中,只須要安裝下面的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提供的,並不須要咱們本身建立.

咱們啓用調試模式查看如下生成的對象 Avatar 能夠看到前三個接口實體是由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章節裏講到過的方法來配置它了.

原文出處:https://www.cnblogs.com/tylerzhou/p/11403854.html

相關文章
相關標籤/搜索