.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
[Test]
public void FixValueTest()
{
var fix = new Fixture();
fix.Customizations.Add(new UriGenerator());
var br = fix.Create<Uri>()# AutoFixture配置二
有了上一节自定义配置,很多问题都能解决了,但是如果仅仅是为了解决一个简单问题那么创建一个类显得有点繁重.其实AutoFixture在创建Fixture对象时有很多方便的Fluent配置,我们这里介绍一些比较常用了.
## 创建对象是忽略一些属性
有些时候有这样的一些业务场景,有些字段是非必填项,但是一旦填写则必须符合指定规则.这些非必填字段在业务中仅仅当它存在的时候做一些校验,其它地方并没有使用到它.这样在单元测试的时候我们为了效率可以暂时忽略这些字段.在后面集成测试的时候再提供完整数据.
下面看看AutoFixture在生成对象的时候如何显式地忽略一些字段
>之所以要忽略是因为如果不忽略AutoFixture自动为字符串类型生成一个guid字符串,这将会导致验证失败.
我们扩展一下Person类,代码如下
```cs
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
用于生成邮箱地址
```cs
[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层有变动,则需要显式修改测试方法,不利于维护,并且这个方法是可以通用的.一旦创建好之后以后遇到这样的业务就可以直接调用了.