为什么自定义 returns 应用于两个不同的属性时是同一个实例?

Why a customization returns the same instance when applied on two different properties?

我在 AutoFixture 3.50.7 和 4.0 (Nunit 3.7) 上遇到了这种奇怪的行为:

当我使用 vanilla Fixture 对象生成包含两个相同类型属性的对象时,测试通过。

当我应用此(公认的虚拟)自定义时,测试失败。生成的列表实际上是同一个实例。

发生了什么事?

[TestFixture]
public class Test
{
    [Test]
    public void Succeeds()
    {
        var fixture = new Fixture();

        var container = fixture.Create<Container>();

        ReferenceEquals(container.Content1.Strings, container.Content2.Strings).Should().BeFalse();
    }

    [Test]
    public void Fails()
    {
        var fixture = new Fixture().Customize(new TestContentCustomization());

        var container = fixture.Create<Container>();

        ReferenceEquals(container.Content1.Strings, container.Content2.Strings).Should().BeFalse();
    }
}

public class TestContentCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customize<Content>(c => c
            .With(x => x.Strings, new List<string>()));
    }
}

public class Container
{
    public Content Content1 { get; set; }
    public Content Content2 { get; set; }
}

public class Content
{
    public IList<string> Strings { get; set; }
}

如果我要求 AutoFixture 生成 fixture.CreateMany<string>(3).ToList() 而不是 new List<string>(),测试也会失败,当然,这次列表包含 3 个不同的字符串。

Customize<T> API 可能令人困惑,这都是我的错,但是当您完成自定义定义后,内部的委托只 运行 一次。我理解为什么使用委托看起来像延迟执行,但事实并非如此;这是一个DSL。这有时会引起混淆,这表明它应该设计得不同,但现在,十年后,这就是我们所拥有的 API。

OP中的自定义相当于这样做:

public void Customize(IFixture fixture)
{
    var strings = new List<string>();
    fixture.Customize<Content>(c => c
        .With(x => x.Strings, strings));
}

Customize(IFixture) 方法每个 fixture 仅 运行s 一次。请注意,With 的参数由一个 lambda 表达式和一个对象组成。第二个参数只是一个对象,因为 C# 是急切求值的,即使你在那里放置一个方法调用,该方法调用也会在调用 With 之前求值。

然后,此自定义的作用是创建一个空的字符串列表(或者,如果您使用 CreateMany,填充的字符串列表),并注册 Content 这样,每个创建 Content 对象时,其 Strings 属性 被分配给该特定对象。

要解决此问题,您可以这样做:

public void Customize(IFixture fixture)
{
    fixture.Customize<Content>(c => c
        .Without(x => x.Strings)
        .Do(x =>
        {
            x.Strings = fixture.CreateMany<string>().ToList();
        }));
}

Do 方法为您提供了真正的延迟执行,但 (IIRC) 其执行顺序并未严格定义,因此您还应使用 .Without(x => x.Strings) 以确保 AutoFixture 不会' t 覆盖 Do 块的效果。

这两个测试都通过了。

综上所述,您应该 avoid writable collection properties:

DO NOT provide settable collection properties.

如果您遵循 Microsoft 官方设计指南,AutoFixture 往往更适合您。