.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即咱们要Build的对象,咱们经过这个Do方法来执行一些赋值操做. 测试

注意Without是必须的,否则AutoFixture在生成对象的时候会覆盖Do方法,仍然执行它内部的生成逻辑.ui

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 进行搜索

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章节里讲到过的方法来配置它了.