【发布时间】:2014-03-27 15:33:03
【问题描述】:
如何告诉 AutoFixture 在某些属性上指定范围(最小值和最大值)
MyDataClass obj = fixture.Create<MyDataClass>();
MyDataClass 有属性 Diameter 并且我只想要这个属性的 min:1 和 max:60?
【问题讨论】:
如何告诉 AutoFixture 在某些属性上指定范围(最小值和最大值)
MyDataClass obj = fixture.Create<MyDataClass>();
MyDataClass 有属性 Diameter 并且我只想要这个属性的 min:1 和 max:60?
【问题讨论】:
您可以在实例化夹具时简单地添加特定的ICustomization<MyDataClass>:
IFixture fixture = new Fixture();
fixture.Customize<MyDataClass>(c => c
.With(x => x.Diameter, () => new Random().Next(1, 61)); // maxValue is excluded, thus 61
现在,每当您使用 fixture.Create<MyDataClass>() 时,都会在创建的实例上设置一个介于 1 和 60 之间的新随机值。
【讨论】:
数据注释
最简单的方法可能是用Data Annotation 来装饰属性本身,尽管我自己不是这个的忠实粉丝:
public class MyDataClass
{
[Range(1, 60)]
public decimal Diameter { get; set; }
}
AutoFixture 将尊重 [Range] 属性的值。
基于约定的
在我看来,更好的方法是基于约定的方法,不依赖于不可执行的属性:
public class DiameterBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
var pi = request as PropertyInfo;
if (pi == null ||
pi.Name != "Diameter" ||
pi.PropertyType != typeof(decimal))
return new NoSpecimen(request);
return context.Resolve(
new RangedNumberRequest(typeof(decimal), 1.0m, 60.0m));
}
}
这个通过测试演示了如何使用它:
[Fact]
public void ResolveRangeLimitedType()
{
var fixture = new Fixture();
fixture.Customizations.Add(new DiameterBuilder());
var actual = fixture.Create<Generator<MyDataClass>>().Take(100);
Assert.True(actual.All(x => 1 <= x.Diameter && x.Diameter <= 60));
}
更多详情请参考this other, very closely related SO Q&A。
克服原始痴迷
也许更好的方法是听你的测试,对抗Primitive Obsession和introduce a custom type——在本例中是Diameter值对象。
这通常是我的首选方法。
【讨论】:
Mark 的解决方案效果很好,但我想要一个更通用的版本,这样我就不必为每个属性编写一个版本。
public class RangeLimiter<T> : ISpecimenBuilder
{
private readonly Expression<Func<T, decimal>> _selector;
private readonly (decimal, decimal) _range;
public RangeLimiter(Expression<Func<T, decimal>> selector, (decimal, decimal) range)
{
_selector = selector;
_range = range;
}
public object Create(object request, ISpecimenContext context)
{
var prop = (PropertyInfo)((MemberExpression)_selector.Body).Member;
var pi = request as PropertyInfo;
if (pi == null || pi.Name != prop.Name || pi.PropertyType != typeof(decimal))
return new NoSpecimen();
return context.Resolve(
new RangedNumberRequest(typeof(decimal), _range.Item1, _range.Item2));
}
}
用法:
[Fact]
public void ResolveRangeLimitedType()
{
var fixture = new Fixture();
fixture.Customizations.Add(new RangeLimiter<MyDataClass>(c => c.Diameter, (1, 12)));
var actual = fixture.Create<Generator<MyDataClass>>().Take(100);
Assert.True(actual.All(x => 1 <= x.Diameter && x.Diameter <= 60));
}
或者更通用,但有点危险(用 int/decimal 测试):
public class RangeLimiter<T, TNum> : ISpecimenBuilder where TNum : struct
{
private readonly Expression<Func<T, TNum>> _selector;
private readonly (TNum, TNum) _range;
public RangeLimiter(Expression<Func<T, TNum>> selector, (TNum, TNum) range)
{
_selector = selector;
_range = range;
}
public object Create(object request, ISpecimenContext context)
{
var prop = (PropertyInfo)((MemberExpression)_selector.Body).Member;
var pi = request as PropertyInfo;
if (pi == null || pi.Name != prop.Name || pi.PropertyType != typeof(TNum))
return new NoSpecimen();
return context.Resolve(
new RangedNumberRequest(typeof(TNum), _range.Item1, _range.Item2));
}
}
【讨论】: