How do I use Autofixture (v3) with ICustomization, ISpecimenBuilder to deal with constructor parameter?

StackOverflow https://stackoverflow.com/questions/15531321

  •  24-03-2022
  •  | 
  •  

Question

I'm trying to overcome a scenario in which a class has a string constructor parameter which cannot be satisfied by any old string generated by Autofixture (the Guid-y looking value).

Before you're tempted to answer simply with a link to Mark Seemann's Ploeh blog entry on Convention-based Customizations, let me say that I've been referencing it and other blog entries of his for this test, which I can't get to pass.

When I step through in debug, I can see that at some point the constructor parameter is passed in with the valid value, but the test still fails with the Guid-y Color value. I think this has something to do with the fact that there is both a 'color' parameter value, and a 'Color' property to be populated by Autofixture. Is it that I've written an ISpecimenBuilder that addresses the constructor parameter, but I'm testing the public property value (two different things)?

I know that all this is overkill for the example, but I envision a more complicated scenario in which using the Build<T>().With() method would not be DRY.

The Failing Test

    [Fact]
    public void Leaf_Color_Is_Brown()
    {
        // arrange
        var fixture = new Fixture().Customize(new LeafColorCustomization());

        // act
        var leaf = fixture.Create<Leaf>();

        // using .Build<>.With(), test passes
        //var leaf = fixture.Build<Leaf>().With(l => l.Color, "brown").CreateAnonymous();

        // assert
        Assert.True(leaf.Color == "brown");
    }

The SUT

    public class Leaf
    {
        public Leaf(string color)
        {
            if (color != "brown")
                throw new ArgumentException(@"NO LEAF FOR YOU!");

            this.Color = color;
        }
        public string Color { get; set; }
    }

The CompositeCustomization implementation (I know the AutoMoqCustomization() isn't needed in this example)

    public class LeafCustomization : CompositeCustomization
    {
        public LeafCustomization()
            : base(
            new LeafColorCustomization(),
            new AutoMoqCustomization()) { }
    }

The Leaf-specific ICustomization

    public class LeafColorCustomization : ICustomization
    {
        public void Customize(IFixture fixture)
        {
            if (fixture == null)
                throw new ArgumentNullException("fixture");

            fixture.Customizations.Add(new LeafBuilder());
        }
    }

The String-constructor-with-name-of-Color-specific ISpecimenBuilder

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

            if (pi.ParameterType != typeof(string) || pi.Name != "color")
                return new NoSpecimen(request);

            return "brown";
        }
    }
Was it helpful?

Solution

Solution 1:

Register that the Color writable property should not be assigned any automatic value as part of the post-processing:

internal class LeafColorCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customize<Leaf>(c => c
            .Without(x => x.Color));

        fixture.Customizations.Add(new LeafBuilder());
    }
}

Solution 2:

Make the Color property read-only:

public class Leaf
{
    private readonly string color;

    public Leaf(string color)
    {
        if (color != "brown")
            throw new ArgumentException(@"NO LEAF FOR YOU!");

        this.color = color;
    }

    public string Color
    {
        get { return this.color; }
    }
}

Since the Color property is read-only AutoFixture is not going to assign a value for it.

The above solutions apply also to AutoFixture 2.

OTHER TIPS

Assuming you deal with the property setting stuff separately, here's a constructor argument restriction Customization which does the trick:

class BrownLeavesCustomization : ICustomization
{
    void ICustomization.Customize( IFixture fixture )
    {
        Func<string> notBrownGenerator = fixture.Create<Generator<string>>()
            .SkipWhile( x => x == "Brown" )
            .First;
        fixture.Customizations.Add( 
            ArgumentGeneratorCustomization<Leaf>.ForConstructorArgument(
                "color", 
                notBrownGenerator ) );
    }

    static class ArgumentGeneratorCustomization<T>
    {
        public static ISpecimenBuilder ForConstructorArgument<TArg>( string argumentName, Func<TArg> generator )
        {
            return new ConstructorArgumentGenerator<TArg>( argumentName, generator );
        }

        class ConstructorArgumentGenerator<TArg> : ISpecimenBuilder
        {
            readonly string _argumentName;
            readonly Func<TArg> _generator;

            public ConstructorArgumentGenerator( string argumentName, Func<TArg> generator )
            {
                Assert.Contains( argumentName, from ctor in typeof( T ).GetConstructors() from param in ctor.GetParameters() select param.Name );
                _argumentName = argumentName;
                _generator = generator;
            }

            object ISpecimenBuilder.Create( object request, ISpecimenContext context )
            {
                var pi = request as ParameterInfo;
                if ( pi == null )
                    return new NoSpecimen( request );
                if ( pi.Member.DeclaringType != typeof( T ) )
                    return new NoSpecimen( request );
                if ( pi.Member.MemberType != MemberTypes.Constructor )
                    return new NoSpecimen( request );
                if ( pi.ParameterType != typeof( TArg ) )
                    return new NoSpecimen( request );
                if ( pi.Name != _argumentName )
                    return new NoSpecimen( request );

                return _generator();
            }
        }
    }
}

Solution: (based on Mark Seemann's comment on this answer)

Accommodate both the constructor parameter and the writeable property in the ISpecimenBuilder implementation, and do nothing other than add the LeafBuilder instance in LeafColorCustomization:

public class LeafBuilder : ISpecimenBuilder
{
    public object Create(object request, ISpecimenContext context)
    {
        var paramInfo = request as ParameterInfo;
        if (paramInfo != null
            && paramInfo.ParameterType == typeof(string)
            && paramInfo.Name == "color")
        { return "brown"; }

        var propInfo = request as PropertyInfo;
        if (propInfo != null
            && propInfo.PropertyType == typeof(string)
            && propInfo.Name == "Color")
        { return "brown"; }

        return new NoSpecimen(request);
    }
}

internal class LeafColorCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customizations.Add(new LeafBuilder());
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top