所以我有以下几种类型:

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

我希望当请求Base时AutoFixture交替创建BaseImplA和BaseImplB。

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

问题是BaseImplB从Value属性 setter 中抛出NotImplementedException。因此,我创建了以下定制:

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

但是正在发生的事情是没有为BaseImplA或BaseImplB设置值。谁能指出我要去哪里错了?

最佳答案

使用AutoFixture 3.18.5+,这并不是很难做到的。这里至少有两个不同的问题在起作用:

处理BaseImplB
BaseImplB类需要特殊处理,这很容易处理。您只需要指示AutoFixture忽略Value属性即可:

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

这会省略Value属性,但会照常创建BaseImplB实例,包括填写其他任何可写属性,例如Text属性。

在不同的实现之间交替

为了在BaseImplABaseImplB之间交替,您可以编写如下的Customization:

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

它只是处理对Base的请求,并将对BaseImplABaseImplB的交替请求中继到context

包装

您可以将两个自定义项(以及其他(如果有的话))打包在一起,例如:

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

这将使您能够根据需要请求BaseImplABaseImplBBase;以下测试证明了这一点:

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

有关版本控制的说明

这个问题浮出了AutoFixture中的一个错误,因此,在AutoFixture 3.18.5之前的AutoFixture版本中,此答案无法正常工作。

关于设计的说明

AutoFixture最初是作为测试驱动开发(TDD)的工具而构建的,而TDD只是关于反馈的。本着GOOS的精神,您应该听听测试。如果测试难以编写,则应考虑API设计。 AutoFixture倾向于放大这种反馈,在这里似乎也是如此。

如OP中所述,该设计违反了Liskov Substitution Principle,因此您应考虑采用另一种设计,而不是这种情况。这种替代设计也可能使AutoFixture设置更简单,更易于维护。

10-06 12:03