使用AutoFixture实例化[immutable]对象时指定[readonly]属性值[via ctor args]

时间:2013-12-27 22:37:31

标签: c# unit-testing immutability autofixture

我的测试要求我将不可变Response对象(见下文)上的Rsvp属性设置为特定值。

public class Rsvp
{
    public string Response { get; private set; }

    public Rsvp(string response)
    {
        Response = response;
    }
}

我最初尝试使用Build<Rsvp>().With(x => x.Rsvp, "Attending")执行此操作,但意识到这只支持可写属性。

我用Build<Rsvp>().FromFactory(new Rsvp("Attending"))替换了它。这可行,但对于更复杂的对象来说很麻烦,因为它们与某些属性无关。

例如,如果Rsvp对象具有CreatedDate属性,则此实例化对象的方法将强制我编写Build<Rsvp>().FromFactory(new Rsvp("Attending", fixture.Create<DateTime>()))

有没有办法只为不可变对象的含义属性指定值?

3 个答案:

答案 0 :(得分:9)

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

坦率地说,不可变类型是C#的一个难点,但是如果从F#中获取提示并引入复制和更新语义,则可以更轻松地使用类Rsvp这样的类。 。如果你像这样修改Rsvp,整体工作会更容易,因此,作为副产品,也可以用于单元测试:

public class Rsvp
{
    public string Response { get; private set; }

    public DateTime CreatedDate { get; private set; }

    public Rsvp(string response, DateTime createdDate)
    {
        Response = response;
        CreatedDate = createdDate;
    }

    public Rsvp WithResponse(string newResponse)
    {
        return new Rsvp(newResponse, this.CreatedDate);
    }

    public Rsvp WithCreatedDate(DateTime newCreatedDate)
    {
        return new Rsvp(this.Response, newCreatedDate);
    }
}

请注意,我添加了两个WithXyz方法,这些方法返回一个新实例,其中一个值已更改,但所有其他值保持不变。

这样您就可以创建Rsvp的实例,用于测试目的:

var fixture = new Fixture();
var seed = fixture.Create<Rsvp>();
var sut = seed.WithResponse("Attending");

或者,作为一个单行:

var sut = new Fixture().Create<Rsvp>().WithResponse("Attending");

如果您无法更改Rsvp,则可以添加WithXyz方法作为扩展方法。

一旦你完成了大约十几次,你就厌倦了,现在是时候转移到F#了,所有这些(以及更多)是内置的:

type Rsvp = {
    Response : string
    CreatedDate : DateTime }

您可以使用AutoFixture创建Rsvp记录,如下所示:

let fixture = Fixture()
let seed = fixture.Create<Rsvp>()
let sut = { seed with Response = "Attending" }

或者,作为一个单行:

let sut = { Fixture().Create<Rsvp>() with Response = "Attending" }

答案 1 :(得分:3)

只要Response属性为readonly * ,您就可以为SpecimenBuilder类型定义自定义Rsvp

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

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

        return "Attending";
    }
}

以下测试通过:

[Fact]
public void ResponseIsCorrect()
{
    var fixture = new Fixture();
    fixture.Customizations.Add(new RsvpBuilder());
    var sut = fixture.Create<Rsvp>();

    var actual = sut.Response;

    Assert.Equal("Attending", actual);
}

* 如果由于某种原因Response属性变为可写,您可以按照此answer中的解决方案进行操作。

答案 2 :(得分:0)

扩展Nikos的答案,我们可以将自定义概括化为可与任何属性一起使用,例如:

"jdbc:sqlserver://localhost;instance=MSSQLSERVER;databaseName=name_of_database;user=sa;password=your_password;"

但是接下来我们需要自定义扩展方法以使其易于使用:

public class OverridePropertyBuilder<T, TProp> : ISpecimenBuilder
{
    private readonly PropertyInfo _propertyInfo;
    private readonly TProp _value;

    public OverridePropertyBuilder(Expression<Func<T, TProp>> expr, TProp value)
    {
        _propertyInfo = (expr.Body as MemberExpression)?.Member as PropertyInfo ??
                        throw new InvalidOperationException("invalid property expression");
        _value = value;
    }

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

        var camelCase = Regex.Replace(_propertyInfo.Name, @"(\w)(.*)",
            m => m.Groups[1].Value.ToLower() + m.Groups[2]);

        if (pi.ParameterType != typeof(TProp) || pi.Name != camelCase)
            return new NoSpecimen();

        return _value;
    }
}

然后在您的示例中将其用作:

public class FixtureCustomization<T>
{
    public Fixture Fixture { get; }

    public FixtureCustomization(Fixture fixture)
    {
        Fixture = fixture;
    }

    public FixtureCustomization<T> With<TProp>(Expression<Func<T, TProp>> expr, TProp value)
    {
        Fixture.Customizations.Add(new OverridePropertyBuilder<T, TProp>(expr, value));
        return this;
    }

    public T Create() => Fixture.Create<T>();
}

public static class CompositionExt
{
    public static FixtureCustomization<T> For<T>(this Fixture fixture)
        => new FixtureCustomization<T>(fixture);
}