自动修复和只读属性

Mar*_*rek 11 c# readonly-attribute autofixture

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

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

    public string Name { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

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

    public Guid Id { get; }

    public string Name { get; }
}
Run Code Online (Sandbox Code Playgroud)

当我尝试使用Autofixture时,它们将正常工作,并且可以同时使用它们。当我尝试使用预先定义参数之一时,问题就开始了。with()方法:

var obj = this.fixture.Build<Client>().With(c => c.Name, "TEST").Build();
Run Code Online (Sandbox Code Playgroud)

这将引发错误

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

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

那么为什么在这里使用这种(误导)策略呢?我在这里找到了一个答案,指出它是通过测试来放大反馈,但是我看不出使用的用处,FromFactory()尤其是当参数列表很广时。在Build()方法之间移动对象实例化Create()会更直观吗?

Fab*_*eco 14

我也为此苦苦挣扎,因为我的大部分课程通常都是只读的。一些库(如 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

这使我能够使用:

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

希望这可以帮助


Mar*_*ann 6

实际上,AutoFixture能够创建构造函数参数并调用构造函数。如何控制一个特定的构造函数参数是一个FAQ,因此,如果这是唯一的问题,我会关闭它作为指定单个构造函数参数值的Easy方法的重复方法?

但是,这篇文章还询问了BuildAPI 行为背后的设计选择,我将在这里回答。

在第二个示例中,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; }
}
Run Code Online (Sandbox Code Playgroud)

即使将值切换了,这也可以正常编译。AutoFixture将无法检测到此类问题。

可能可以给AutoFixture启发式,当BuildAPI引用只读属性时,API会尝试猜测“你的意思”,但是回到我还是该项目的仁慈独裁者时,我认为这是一个功能具有不必要的复杂性。新的维护人员在该主题上的外观可能会有所不同。

作为一般观察,我认为整个BuildAPI都是错误的。在过去的几年中,我使用AutoFixture编写了测试,但从未使用过该API。如果我今天仍然运行该项目,那么我将弃用该API,因为它会使人们以脆弱的方式使用AutoFixture。

因此,这是一个明确的设计选择。

  • 马克,您能否详细说明这一点 - “我认为整个 Build API 是一个错误”。当您需要使用预先填充的数据创建一个实例时,您使用什么,以便将来有人添加新属性时,它不会因为该值未设置为新属性而导致旧测试失败? (3认同)