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