سؤال

So I have the following types:

public abstract class Base
{
    public string Text { get; set; }
    public abstract int Value { get; set; }
}

public class BaseImplA : Base
{
    public override int Value { get; set; }
}

public class BaseImplB : Base
{
    public override int Value
    {
        get { return 1; }
        set { throw new NotImplementedException(); }
    }
}

I want AutoFixture to alternate creating BaseImplA and BaseImplB when Base is requested.

var fixture = new Fixture().Customize(new TestCustomization());
var b1 = fixture.Create<Base>();
var b2 = fixture.Create<Base>();

The issue is BaseImplB throws a NotImplementedException from the Value property setter. So I created the following customization:

public class TestCustomization : ICustomization
{
    private bool _flag;
    private IFixture _fixture;

    public void Customize(IFixture fixture)
    {
        _fixture = fixture;

        fixture.Customize<BaseImplB>(composer =>
        {
            return composer.Without(x => x.Value);
        });

        fixture.Customize<Base>(composer =>
        {
            return composer.FromFactory(CreateBase);
        });
    }

    private Base CreateBase()
    {
        _flag = !_flag;

        if (_flag)
        {
            return _fixture.Create<BaseImplA>();
        }

        return _fixture.Create<BaseImplB>();
    }
}

But what's happening is that the Value is not being set for BaseImplA or BaseImplB. Can anyone point out where I'm going wrong?

هل كانت مفيدة؟

المحلول

With AutoFixture 3.18.5+, this isn't too difficult to do. There's at least two different issues in play here:

Dealing with BaseImplB

The BaseImplB class needs special treatment, which is quite easy to deal with. You only need to instruct AutoFixture to ignore the Value property:

public class BCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customize<BaseImplB>(c => c.Without(x => x.Value));
    }
}

This omits the Value property, but otherwise creates instances of BaseImplB as usual, including filling out any other writable properties, such as the Text property.

Alternating between different implementation

In order to alternate between BaseImplA and BaseImplB, you can write a Customization like this:

public class AlternatingCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customizations.Add(new AlternatingBuilder());
    }

    private class AlternatingBuilder : ISpecimenBuilder
    {
        private bool createB;

        public object Create(object request, ISpecimenContext context)
        {
            var t = request as Type;
            if (t == null || t != typeof(Base))
                return new NoSpecimen(request);

            if (this.createB)
            {
                this.createB = false;
                return context.Resolve(typeof(BaseImplB));
            }

            this.createB = true;
            return context.Resolve(typeof(BaseImplA));
        }
    }
}

It simply deals with requests for Base, and relays alternating requests for BaseImplA and BaseImplB to the context.

Packaging

You can package up both Customizations (and others, if you have them) in a Composite, like this:

public class BaseCustomization : CompositeCustomization
{
    public BaseCustomization()
        : base(
            new BCustomization(),
            new AlternatingCustomization())
    {
    }
}

This will enable you to request BaseImplA, BaseImplB, and Base, as you need them; the following tests demonstrate this:

[Fact]
public void CreateImplA()
{
    var fixture = new Fixture().Customize(new BaseCustomization());

    var actual = fixture.Create<BaseImplA>();

    Assert.NotEqual(default(string), actual.Text);
    Assert.NotEqual(default(int), actual.Value);
}

[Fact]
public void CreateImplB()
{
    var fixture = new Fixture().Customize(new BaseCustomization());

    var actual = fixture.Create<BaseImplB>();

    Assert.NotEqual(default(string), actual.Text);
    Assert.Equal(1, actual.Value);
}

[Fact]
public void CreateBase()
{
    var fixture = new Fixture().Customize(new BaseCustomization());

    var actual = fixture.CreateMany<Base>(4).ToArray();

    Assert.IsAssignableFrom<BaseImplA>(actual[0]);
    Assert.NotEqual(default(string), actual[0].Text);
    Assert.NotEqual(default(int), actual[0].Value);

    Assert.IsAssignableFrom<BaseImplB>(actual[1]);
    Assert.NotEqual(default(string), actual[1].Text);
    Assert.Equal(1, actual[1].Value);

    Assert.IsAssignableFrom<BaseImplA>(actual[2]);
    Assert.NotEqual(default(string), actual[2].Text);
    Assert.NotEqual(default(int), actual[2].Value);

    Assert.IsAssignableFrom<BaseImplB>(actual[3]);
    Assert.NotEqual(default(string), actual[3].Text);
    Assert.Equal(1, actual[3].Value);
}

A note on versioning

This question surfaced a bug in AutoFixture, so this answer will not work unmodified in versions of AutoFixture prior to AutoFixture 3.18.5.

A note on design

AutoFixture was originally build as a tool for Test-Driven Development (TDD), and TDD is all about feedback. In the spirit of GOOS, you should listen to your tests. If the tests are hard to write, you should consider your API design. AutoFixture tends to amplify that sort of feedback, and that also seems to be the case here.

As given in the OP, the design violates the Liskov Substitution Principle, so you should consider an alternative design where this is not the case. Such an alternative design is also likely to make the AutoFixture setup simpler, and easier to maintain.

نصائح أخرى

Mark Seemann provided an excellent response. You can build a reusable rotating specimen builder for your abstract base types like this:

public class RotatingSpecimenBuilder<T> : ISpecimenBuilder
{
    protected const int Seed = 812039;
    protected readonly static Random Random = new Random(Seed);

    private static readonly List<Type> s_allTypes = new List<Type>();
    private readonly List<Type> m_derivedTypes = new List<Type>();
    private readonly Type m_baseType = null;

    static RotatingSpecimenBuilder()
    {
        s_allTypes.AddRange(AppDomain.CurrentDomain.GetAssemblies().SelectMany(s => s.GetTypes()));
    }

    public RotatingSpecimenBuilder()
    {
        m_baseType = typeof(T);
        m_derivedTypes.AddRange(s_allTypes.Where(x => x != m_baseType && m_baseType.IsAssignableFrom(x)));
    }

    public object Create(object request, ISpecimenContext context)
    {
        var t = request as Type;
        if (t == null || t != m_baseType || m_derivedTypes.Count == 0)
        {
            return new NoSpecimen(request);
        }

        var derivedType = m_derivedTypes[Random.Next(0, m_derivedTypes.Count - 1)];
        return context.Resolve(derivedType);
    }
}

Then register this specimen builder as your fixture customization for each base type like this:

var fixture = new Fixture.Customizations.Add(new RotatingSpecimenBuilder<YourBaseType>());

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top