.net测试篇之测试神器Autofixture Generator使用与自定义builder

系列目录html

有了上一节自定义配置,不少问题都能解决了,可是若是仅仅是为了解决一个简单问题那么建立一个类显得有点繁重.其实AutoFixture在建立Fixture对象时有不少方便的Fluent配置,咱们这里介绍一些比较经常使用了.数据库

建立对象是忽略一些属性

有些时候有这样的一些业务场景,有些字段是非必填项,可是一旦填写则必须符合指定规则.这些非必填字段在业务中仅仅当它存在的时候作一些校验,其它地方并无使用到它.这样在单元测试的时候咱们为了效率能够暂时忽略这些字段.在后面集成测试的时候再提供完整数据.数组

下面看看AutoFixture在生成对象的时候如何显式地忽略一些字段dom

之因此要忽略是由于若是不忽略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.ui

指定当前时间

在集成测试的时候,有些关于时间的字段都须要是当前时间,这时候可使用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层有变更,则须要显式修改测试方法,不利于维护,而且这个方法是能够通用的.一旦建立好以后之后遇到这样的业务就能够直接调用了.