How to fix a range on some properties when create a TestClass by AutoFixture
Data Annotations
The easiest approach is probably adorning the property itself with a Data Annotation, although I'm not myself a huge fan of this:
public class MyDataClass
{
[Range(1, 60)]
public decimal Diameter { get; set; }
}
AutoFixture will respect the [Range]
attribute's values.
Convention-based
A better approach is, in my opinion, a convention-based approach that doesn't rely on non-enforceable attributes:
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));
}
}
This passing test demonstrates how to use it:
[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));
}
For more details, please refer to this other, very closely related SO Q&A.
Overcoming Primitive Obsession
Perhaps an even better approach is to listen to your tests, combat Primitive Obsession, and introduce a custom type - in this case, a Diameter
Value Object.
This is often my preferred approach.
You could simply add a specific ICustomization<MyDataClass>
when instantiating the fixture:
IFixture fixture = new Fixture();
fixture.Customize<MyDataClass>(c => c
.With(x => x.Diameter, () => new Random().Next(1, 61)); // maxValue is excluded, thus 61
Now, whenever you use fixture.Create<MyDataClass>()
, a new random value between 1 and 60 is set on the created instance.
Solution by Mark works well, but I wanted a more generic version of it so that I didn't have to write a version of it for every property.
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));
}
}
Usage:
[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));
}
Or an even more generic, but a bit dangerous (tested with 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));
}
}