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

时间:2014-05-04 21:56:23

标签: c# autofixture

所以我有以下类型:

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属性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设置Value。谁能指出我哪里出错?

2 个答案:

答案 0 :(得分:9)

使用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

<强>包装

您可以在复合中打包自定义(以及其他人,如果有的话),如下所示:

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设置更简单,更易于维护。

答案 1 :(得分:0)

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