【问题标题】:AutoDataAttribute's concrete object creation logic calls all property getters onceAutoDataAttribute 的具体对象创建逻辑调用所有属性 getter 一次
【发布时间】:2017-11-26 07:58:45
【问题描述】:

使用 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”被命中之前被命中。当我“懒惰地”初始化属性与上面的属性没有什么不同时,这是有问题的 - 因为属性的支持字段在测试运行之前被初始化,我无法测试初始化​​逻辑。

有没有办法自定义AutoDataAttribute 以便我可以绕过这种行为?或者,也许这是一个错误?

【问题讨论】:

    标签: c# unit-testing xunit.net autofixture


    【解决方案1】:

    事实上,这不是 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,也许您可​​以通过测试一个覆盖ToString 的特定于测试的子类来解决此问题,如下所示:

    [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 的样式编写测试之外,我想不出任何其他解决方法。

    【讨论】:

    • 感谢您的详细解答!
    猜你喜欢
    • 1970-01-01
    • 2015-12-28
    • 2018-09-11
    • 2014-11-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-12
    相关资源
    最近更新 更多