Specifying [readonly] property values [via ctor args] when instantiating [immutable] objects with AutoFixture

Viewed 3794

My test requires that I set the Response property on an immutable Rsvp object (see below) to a specific value.

public class Rsvp
{
    public string Response { get; private set; }

    public Rsvp(string response)
    {
        Response = response;
    }
}

I initially tried to do this using Build<Rsvp>().With(x => x.Rsvp, "Attending"), but realized this only supports writable properties.

I replaced that with Build<Rsvp>().FromFactory(new Rsvp("Attending")). This works, but is cumbersome for more complex objects where it doesn't matter what some of the properties are.

For instance, if the Rsvp object had a CreatedDate property, this method of instantiating the object would force me to write Build<Rsvp>().FromFactory(new Rsvp("Attending", fixture.Create<DateTime>())).

Is there a way to only specify values for meaning properties for an immutable object?

3 Answers

Extending Nikos“ answer, we can generalize the customization to work with any property as such:

public class OverridePropertyBuilder<T, TProp> : ISpecimenBuilder
{
    private readonly PropertyInfo _propertyInfo;
    private readonly TProp _value;

    public OverridePropertyBuilder(Expression<Func<T, TProp>> expr, TProp value)
    {
        _propertyInfo = (expr.Body as MemberExpression)?.Member as PropertyInfo ??
                        throw new InvalidOperationException("invalid property expression");
        _value = value;
    }

    public object Create(object request, ISpecimenContext context)
    {
        var pi = request as ParameterInfo;
        if (pi == null)
            return new NoSpecimen();

        var camelCase = Regex.Replace(_propertyInfo.Name, @"(\w)(.*)",
            m => m.Groups[1].Value.ToLower() + m.Groups[2]);

        if (pi.ParameterType != typeof(TProp) || pi.Name != camelCase)
            return new NoSpecimen();

        return _value;
    }
}

But then we need custom extension methods to make it easier to use:

public class FixtureCustomization<T>
{
    public Fixture Fixture { get; }

    public FixtureCustomization(Fixture fixture)
    {
        Fixture = fixture;
    }

    public FixtureCustomization<T> With<TProp>(Expression<Func<T, TProp>> expr, TProp value)
    {
        Fixture.Customizations.Add(new OverridePropertyBuilder<T, TProp>(expr, value));
        return this;
    }

    public T Create() => Fixture.Create<T>();
}

public static class CompositionExt
{
    public static FixtureCustomization<T> For<T>(this Fixture fixture)
        => new FixtureCustomization<T>(fixture);
}

we then use it in your example as:

var obj = 
  new Fixture()
    .For<Rsvp>()
    .With(x => x.Response, "Attending")
    .Create();
Related