How to fix a range on some properties when create a TestClass by AutoFixture
Asked Answered
N

3

12

How can I tell AutoFixture to specify a range (min and max) on some properties when doing

MyDataClass obj = fixture.Create<MyDataClass>();

where MyDataClass has property Diameter and I only want min:1 and max:60 on this property?

Nickels answered 27/3, 2014 at 15:33 Comment(0)
C
19

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.

Casuist answered 27/3, 2014 at 16:0 Comment(1)
I don't want to use DataAnnotations since my need is for a backend-used class and has no UI. Is there an easier way to implement this without having to create a class (albeit a small one)? I have maybe 2-3 tests where this range on an integer value is required. It seems a bit much to add a class just for a small handful of places where this will be used - and only on a single property that is an int type. I don't need the range limitation on all int types.Retainer
M
4

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.

Malda answered 1/2, 2021 at 20:22 Comment(1)
This isn't nearly as 'proper' as the other answers, but it's exactly what I want - thank you!Gory
F
1

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));
        }
    }
Florri answered 8/1, 2019 at 13:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.