AutoDataAttribute 的具体对象创建逻辑调用所有 属性 getter 一次

AutoDataAttribute's concrete object creation logic calls all property getters once

使用 AutoFixture 3.50 和 xUnit.NET,似乎 Fixture.Create() 创建具体对象的方式与 AutoData Theory 测试创建具体对象的方式有所不同。

简单例子:

public class Foo
{
    private string prop;
    public string Prop
    {
        get
        {
             if (prop == null) { prop = "Prop"; } // Breakpoint 'A'
             return prop;
        }
    }
}

测试使用 Fixture:

[Fact]
public void FixtureTest()
{
    var fixture = new Fixture();
    var result = fixture.Create<Foo>(); // Breakpoint 'B1'
}

测试使用AutoDataAttribute:

[Theory, AutoData]
public void AutoDataTest(Foo sut)
{
    var bar = 1; // Essential no-op, Breakpoint 'B2'
}

在前一个测试中,断点 'B1' 被击中,而断点 'A' 从未被击中。在后一个测试中,断点 'A' 在断点 'B2' 被命中之前被命中。当我 "lazily" 初始化 属性 与上面的没有什么不同时,这是有问题的 - 因为 属性 的支持字段在测试 运行 之前被初始化,我无法测试初始化逻辑。

有没有办法自定义 AutoDataAttribute 以便我可以解决此问题?或者,这可能是一个错误?

事实上,这不是 AutoFixture 问题,而是 xUnit.net 问题,如果您愿意的话。您可以像这样在没有 AutoFixture 的情况下完全复制它:

[Theory, ClassData(typeof(FooTestCases))]
public void ClassDataTest(Foo sut)
{
    var bar = 1; // Essential no-op, Breakpoint 'B3'
}

private class FooTestCases : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] { new Foo() };
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

如果调试此 ClassDataTest,您还会在遇到断点 'B3'.

之前遇到断点 'A'

原因是 xUnit.net 测试运行器希望为每个参数化测试提供一个良好的显示名称,因此它尝试为传递给每个测试用例的所有参数创建一个可读的字符串表示形式[Theory].

当数据对象未明确覆盖 ToString 时,xUnit.net 回退到读取所有属性并从中构建显示字符串。这就是这里发生的事情。

我尝试搜索有关该行为的一些官方文档,但我能找到的最好的是 this。正如那里所建议的那样,您可以通过覆盖 ToString:

来解决这个问题
public class Foo
{
    private string prop;
    public string Prop
    {
        get
        {
            if (prop == null) { prop = "Prop"; } // Breakpoint 'A'
            return prop;
        }
    }

    public override string ToString()
    {
        return "Foo";
    }
}

此更改在使用 ClassDataAutoData 时停止命中断点 'A'。是否要覆盖 ToString 是另一个问题。

如果您不想在 Foo 上覆盖 ToString,也许您可​​以通过测试确实覆盖 [=15] 的特定测试子 class 来解决这个问题=],像这样:

[Theory, AutoData]
public void AutoDataTestFoo(TestFoo sut)
{
    var bar = 1; // Essential no-op, Breakpoint 'B4'
}

public class TestFoo : Foo
{
    public override string ToString()
    {
        return "Foo";
    }
}

这也会阻止命中断点 'A'。

只要 Foo 不是 sealed,您就应该能够做到这一点。如果 Foosealed,除了以 FixtureTest.

的风格编写测试之外,我想不出任何其他解决方法