自动混合和只读属性

时间:2017-11-20 11:53:14

标签: c# autofixture readonly-attribute

让我们考虑同一个非常简单的实体的两个版本(一个只读属性):

public class Client
{
    public Guid Id { get; set; }

    public string Name { get; set; }
}

vs

public class Client
{
    public Client(Guid id, string name)
    {
        this.Id = id;
        this.Name = name;
    }

    public Guid Id { get; }

    public string Name { get; }
}

当我尝试使用Autofixture时,它会正常工作,并且正如预期的那样。当我尝试使用。with()方法预定义其中一个参数时,问题就出现了:

var obj = this.fixture.Build<Client>().With(c => c.Name, "TEST").Build();

这会抛出错误

  

System.ArgumentException:属性“Name”是只读的。

但似乎Autofixture知道如何使用构造函数!并且似乎实际的Build<>()方法创建的对象实例不是Create()!如果构建只是准备构建器,使用设置属性,然后Create将实例化对象,它将与只读属性一起正常工作。

那么为什么这里采用了这种(误导性)策略呢?我找到了一个答案here,它说明了用测试来放大反馈,但我没有看到使用FromFactory()的用处,特别是当参数列表很广泛时。不会将对象实例化从Build()方法移动到Create()方法更直观吗?

3 个答案:

答案 0 :(得分:4)

AutoFixture确实能够创建构造函数参数,并调用构造函数。如何控制一个特定的构造函数参数是一个常见问题解答,所以如果这是唯一的问题,我已将其作为Easy way to specify the value of a single constructor parameter?

的副本关闭

然而,这篇文章还询问了Build API行为背后的设计选择,我将在此处回答。

在第二个示例中,Name是只读属性,您无法更改只读属性的值。这是.NET(以及大多数其他语言)的一部分,而不是AutoFixture的设计选择。

让我们绝对清楚:Name是一个属性。从技术上讲,它与类的构造函数无关。

我假设您认为Name与构造函数的name参数相关联,因为一个暴露了另一个,但我们只知道因为我们有源代码。对于外部观察者来说,确保这两者是连接的,没有技术上安全的方法。外部观察者(例如AutoFixture)可能会尝试猜测存在这样的连接,但是没有任何保证。

在技术上可以编写这样的代码:

public class Person
{
    public Person(string firstName, string lastName)
    {
        this.FirstName = lastName;
        this.LastName = firstName;
    }

    public string FirstName { get; }

    public string LastName { get; }
}

这个编译得很好,即使值已切换。 AutoFixture将无法检测到类似的问题。

当你引用只读属性时Build API尝试猜测你的意思时,可能会给AutoFixture一个启发式,但当我仍然是项目的仁慈独裁者时,我认为这是一个没有根据的复杂性的功能。新维护者可能会对该主题看起来不同。

作为一般观察,我认为整个Build API都是错误的。在过去的许多年里,我使用AutoFixture编写测试,我从未使用过该API。如果我今天仍然运行该项目,我会弃用该API,因为它会导致人们以一种脆弱的方式使用AutoFixture。

所以这是一个明确的设计选择。

答案 1 :(得分:0)

我也为此感到困惑,因为我的大多数课程通常都是只读的。诸如Json.Net之类的某些库使用命名约定来了解哪些构造函数参数会影响每个属性。

确实存在一种使用ISpecimenBuilder界面自定义属性的方法:

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

您已经注意到,尝试在Build<> api上使用它是一个死胡同。所以我必须为自己创建扩展方法:

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

这使我可以这样使用:

var obj = 
  new Fixture()
  .For<Client>()
  .With(x => x.Name, "TEST")
  .Create();

希望这会有所帮助

答案 2 :(得分:0)

嗨,我遇到了类似的问题,我使用`Freeze

解决了它
      _formFileMock = _fixture.Freeze<Mock<IFormFile>>();
      _formFileMock.Setup(m => m.ContentType).Returns("image/jpeg");
    _fixture.Create<P>

相关问题