【问题标题】:AutoData Theories with AutoFixture using manual fakesAutoData Theories with AutoFixture 使用手动假货
【发布时间】:2013-03-04 18:30:51
【问题描述】:

给定这个系统来测试:

public class MySut
{
    private readonly IHardToMockDependency _hardToMockDependency;

    public MySut(IHardToMockDependency hardToMockDependency,
                 IOtherDependency otherDependency)
    {
        _hardToMockDependency = hardToMockDependency;
    }

    public string GetResult()
    {
        return _hardToMockDependency.GetResult();
    }
}

public interface IOtherDependency { }

public interface IHardToMockDependency
{
    string GetResult();
}

还有这个单元测试:

internal class FakeHardToMockDependency : IHardToMockDependency
{
    private readonly string _result;

    public FakeHardToMockDependency(string result)
    {
        _result = result;
    }

    public string GetResult()
    {
        return _result;
    }
}

public class MyTests
{
    [Fact]
    public void GetResultReturnsExpected()
    {
        string expectedResult = "what I want";
        var otherDependencyDummy = new Mock<IOtherDependency>();
        var sut = new MySut(new FakeHardToMockDependency(expectedResult),
                            otherDependencyDummy.Object);

        var actualResult = sut.GetResult();

        Assert.Equal(expectedResult, actualResult);
    }
}

我应该如何将其转换为使用 AutoFixture.Xunit 和 AutoFixture.AutoMoq(同时仍然使用手动伪造)?

在现实世界的测试中,手动伪造的界面和行为会更加复杂。请注意,我想将匿名变量(expectedResult 字符串)传递给手动 fake 的构造函数。

【问题讨论】:

    标签: moq xunit.net autofixture automocking


    【解决方案1】:

    这里已经有一些很好的答案,但我想提出一个更简单的替代方案,它涉及稍微放松 FakeHardToMockDependency 类的不变量。将其公开,并提供一种分配与构造函数分离的结果的方法:

    public class FakeHardToMockDependency : IHardToMockDependency
    {
        private string _result;
    
        public FakeHardToMockDependency(string result)
        {
            _result = result;
        }
    
        internal string Result
        {
            get { return _result; }
            set { _result = value; }
        }
    
        public string GetResult()
        {
            return _result;
        }
    }
    

    请注意,我添加了一个内部属性并从字段中删除了 readonly 关键字。

    这使您能够将原始测试重构为:

    [Theory, AutoMoqData]
    public void GetResultReturnsExpected_AutoDataVersion(
        [Frozen(As = typeof(IHardToMockDependency))]FakeHardToMockDependency fake,
        MySut sut)
    {
        var expected = "what I want";
        fake.Result = expected;
    
        var actual = sut.GetResult();
    
        Assert.Equal(expected, actual);
    }
    

    为了完整起见,这里是 AutoMoqDataAttribute 代码:

    public class AutoMoqDataAttribute : AutoDataAttribute
    {
        public AutoMoqDataAttribute()
            : base(new Fixture().Customize(new AutoMoqCustomization()))
        {
        }
    }
    

    【讨论】:

    • 现在你去把金锤从我手里拿走。 ;-) 但是internalpublic 相比有什么不同?无论哪种方式,SUT 都无法看到该属性,因为它只能获取接口。
    • @GCATNM Result 属性是internal 还是public 并不重要。就我个人而言,我将其设为 public,但 OP 提供了假冒为 internal 类,所以我只想尽可能少地进行更改以使建议的修改更容易接受:)
    • 哇,三个很棒的答案,一个来自作者本人!松开假的不变量很好。谢谢!只接受一个答案将很难 - 我必须尝试这些。
    • 这是我需要的。我不知道FrozenAs 选项。其他解决方案对于其他场景可能非常有用(并且@GCATNM 的解释非常好),但这里匿名字符串变量是自动模拟的并默认注入到假中,这就是我想要的。我不需要在fake.Result 上公开设置器,因为 AutoFixture 已经为假的构造函数提供了一个值。在这个简单的例子中,我的 Assert 可以是 Assert.Equal(fake.GetResult(), actualResult)
    • As 属性确实是一个相当难以捉摸的野兽。我只是在需要时才发现它,并开始研究如何实现它。好吧,愚蠢的是我认为我应该能够改进 AutoFixture。 ;-)
    【解决方案2】:

    根据您需要传递给手动伪造的参数类型,您可能可以使用参数化属性,类似于 AutoFixture 的内置 InlineAutoDataAttribute

    鉴于这些

    public interface IHardToMockDependency
    {
        string Value { get; }
    }
    
    public class FakeHardToMockDependency : IHardToMockDependency
    {
        private readonly string _value;
    
        public FakeHardToMockDependency(string value)
        {
            _value = value;
        }
    
        #region IHardToMockDependency Members
    
        public string Value
        {
            get { return this._value; }
        }
    
        #endregion IHardToMockDependency Members
    }
    

    您创建了一个ICustomization 实现,它告诉夹具对象如何创建IHardToFakeDependency 接口的实现:

    public class FakeHardToMockDependencyCustomization : ICustomization
    {
        private readonly string _value;
    
        public FakeHardToMockDependencyCustomization(string value)
        {
            _value = value;
        }
    
        #region ICustomization Members
    
        public void Customize(IFixture fixture)
        {
            fixture.Register<IHardToMockDependency>(() => new FakeHardToMockDependency(this._value));
        }
    
        #endregion ICustomization Members
    }
    

    注意,这当然需要知道你要传入的字符串。

    接下来,您将其与您想在 CompositeCustomization 中使用的其他自定义项一起汇总:

    public class ManualFakeTestConventions : CompositeCustomization
    {
        public ManualFakeTestConventions(string value)
            : base(new FakeHardToMockDependencyCustomization(value), new AutoMoqCustomization())
        {
        }
    }
    

    确保您始终按照从最具体到最一般的顺序排列自定义,正如 Mark Seemann 所解释的 here

    现在您创建一个使用此自定义的AutoDataAttribute 实现:

    public class ManualFakeAutoDataAttribute : AutoDataAttribute
    {
        public ManualFakeAutoDataAttribute(string value)
            : base(new Fixture().Customize(new ManualFakeTestConventions(value)))
        {
        }
    }
    

    现在可以像 InlineAutoDataAttribute 一样使用:

    public class ManualFakeTests
    {
        [Theory, ManualFakeAutoData("iksdee")]
        public void ManualFake(IHardToMockDependency fake)
        {
            Assert.IsType<FakeHardToMockDependency>(fake);
            Assert.Equal("iksdee", fake.Value);
        }
    }
    

    您还可以通过将[Frozen] 属性应用于 Theory 参数,立即将其注入到自动创建的 SUT 实例中:

        [Theory, ManualFakeAutoData("iksdee")]
        public void SutWithManualFake([Frozen] IHardToMockDependency fake, MySut sut)
        {
    
        }
    

    这将创建一个MySut 实例和构造函数所需的IHardToMockDependency 实例,为此您在FakeHardToMockDependencyCustomization 中为AutoFixture 提供了一个规则,并且还将该实例作为fake 变量提供给您。

    请注意,不冻结假货仍会为您提供正确的 FakeHardToMockDependency 实例以及将一个注入到 sut 中,但这些将是不同的,因为我们已经在自定义中注册了一个工厂委托。冻结实例将导致夹具始终为接口的后续请求返回相同的实例。

    不过,这里有一些注意事项:

    • 您没有对作为参数传入的字符串的引用,因此您必须将其作为字符串文字两次。例如,您可以使用测试类中的字符串常量来解决此问题。
    • .NET 中可用作属性参数的类型数量有限。只要你只需要基本类型就可以了,但是不能在参数列表中调用构造函数之类的。
    • 如果IHardToFakeDependency,你应该只在需要实例时使用这个属性;否则无论如何你总是必须传入一个字符串参数。如果您需要使用一组标准自定义项,请创建另一个仅包含这些自定义项的属性。
    • 如果您同时需要InlineAutoDataAttribute 的功能,您还需要创建另一个结合两者功能的属性。

    根据具体情况,您可能还想查看 xUnit.net 的 PropertyDataAttribute,但我发现自己几乎没有使用过。

    在我看来,一般来说,了解如何使用自定义和自动数据属性以及何时以及如何创建自己的属性是有效使用 AutoFixture 并真正让它为您节省工作的关键。

    如果您经常在需要测试的特定域中编写代码,那么创建一个包含自定义、属性和存根对象的库可能很有意义,一旦您将其放在 xUnit 旁边,这些库就可以随时使用。净、自动夹具和最小起订量。我知道我很高兴自己建造了自己的。

    哦,还有:具有难以模拟的依赖项可能指向设计问题。为什么嘲笑这么难?

    【讨论】:

    • 谢谢;非常有帮助!我同意这是一种代码异味,表明存在潜在的设计问题。
    【解决方案3】:

    也许这不是最惯用的 Autofixture 设置,但绝对有效:

    [Fact]
    public void GetResultReturnsExpected()
    {
        var fixture = new Fixture()
            .Customize(new AutoMoqCustomization());
    
        var expectedResult = fixture.Create<string>();
    
        fixture.Register<IHardToMockDependency>(
            () => new FakeHardToMockDependency(expectedResult));
    
        var sut = fixture.Create<MySut>();
    
        var actualResult = sut.GetResult();
    
        Assert.Equal(expectedResult, actualResult);
    }
    

    如果您还想使用AutoData,您可以基于this great article 创建自己的AutoMoqData,您可以在其中隐藏部分或全部夹具自定义。

    类似:

    public class MySutAutoDataAttribute : AutoDataAttribute
    {
        public MySutAutoData()
            : base(new Fixture()
                .Customize(new AutoMoqCustomization()))
        {
            Fixture.Freeze<string>();
    
            Fixture.Register<IHardToMockDependency>(
                () => new FakeHardToMockDependency(Fixture.Create<string>()));
        }
    }
    

    你可以像这样使用它:

    [Theory, MySutAutoData]
    public void GetResultReturnsExpected(MySut sut, string expectedResult)
    {
        var actualResult = sut.GetResult();
    
        Assert.Equal(expectedResult, actualResult);
    }
    

    但您应该注意,MySutAutoDataAttribute 有很多改进空间,例如:它不是很通用,如果您在测试中使用多个字符串,Fixture.Freeze&lt;string&gt;(); 可能会导致问题。

    【讨论】:

    • 我意识到我可以做到这一点,但我的问题是专门针对 AutoDataAttribute 的后代做这件事。您能否提供一个针对这种情况的示例?
    • @TrueWill 很抱歉,我错过了您想要使用 AutoDataAttribute 的解决方案的标题。我已经用可能的实现更新了我的答案。
    • 然而,像这样冻结字符串无法控制它。
    猜你喜欢
    • 1970-01-01
    • 2021-08-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-08
    相关资源
    最近更新 更多