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));
        }
    }