Autofixture 和只读属性

Autofixture and read only properties

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

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

    public string Name { get; set; }
}

对比

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: The property "Name" is read-only.

不过Autofixture好像知道怎么使用构造函数!似乎实际的 Build<>() 方法创建了一个对象的实例,而不是 Create()!如果构建只是准备生成器,将设置属性,然后创建将实例化对象,它将与只读属性一起正常工作。

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

AutoFixture 确实能够创建构造函数参数并调用构造函数。如何控制特定的构造函数参数是一个常见问题解答,所以如果那是唯一的问题,我会把它作为

的副本关闭

然而,这个 post 也询问了 Build API 行为背后的设计选择,我将在这里回答。

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

让我们明确一点:Name 是 属性。从技术上讲,它与 class' 构造函数无关。

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

像这样编写代码在技术上是可行的:

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 会尝试猜测 'what you mean',但返回时,可能会给 AutoFixture 一个启发式当我还是这个项目的仁慈独裁者时,我认为这是一个具有非战争运行复杂性的特性。新的维护者可能对这个主题有不同的看法。

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

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

我也为此苦苦挣扎,因为我的大部分 类 通常都是只读的。 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();

希望这对您有所帮助

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

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