用于创建基类型实现的自定义

本文关键字:自定义 实现 类型 创建 基类 用于 | 更新日期: 2023-09-27 18:01:33

所以我有以下类型:

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

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

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

问题是BaseImplB从Value属性设置器抛出一个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之间交替,您可以这样编写自定义:

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

包装

您可以将两个自定义(以及其他自定义,如果有的话)打包到一个Composite中,如下所示:

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

这将使您能够请求BaseImplA, BaseImplBBase,当你需要他们;下面的测试证明了这一点:

[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最初是作为测试驱动开发(TDD)的工具构建的,而TDD是关于反馈的。本着GOOS的精神,你应该倾听你的测试。如果测试很难编写,您应该考虑您的API设计。AutoFixture倾向于放大这种反馈,这里的情况似乎也是如此。

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

Mark Seemann给出了一个很好的回答。您可以为抽象基类型构建一个可重用的旋转样本构建器,如下所示:

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

然后注册这个样本生成器作为每个基本类型的夹具定制,如下所示:

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