17

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

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:属性“名称”是只读的。

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

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

4

3 回答 3

17

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

希望这可以帮助

于 2019-07-25T12:35:21.003 回答
11

AutoFixture 确实能够创建构造函数参数并调用构造函数。如何控制特定的构造函数参数是一个常见问题解答,所以如果这是唯一的问题,我会关闭它作为指定单个构造函数参数值的简单方法的副本?

然而,这篇文章也询问了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; }
}

这编译得很好,即使值被切换了。AutoFixture 将无法检测到这样的问题。

Build当您引用只读属性时,可能会给 AutoFixture 一个启发式方法,其中API 会尝试猜测“您的意思”,但是当我还是该项目的仁慈独裁者时,我认为这是一个功能具有不必要的复杂性。新的维护者在这个话题上的看法可能会有所不同。

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

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

于 2017-11-20T15:52:29.733 回答
2

嗨我有一个类似的问题我用`Freeze解决了它

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

于 2021-02-23T16:13:39.717 回答