【问题标题】:XUnit, AutoFixture and Moq best practiceXUnit、AutoFixture 和 Moq 最佳实践
【发布时间】:2015-02-17 01:08:29
【问题描述】:

我正在阅读大量有关如何结合标题中的三个组件对事物进行正确单元测试的文档和示例。我为我的业务逻辑上的一个方法想出了一个测试方法,但感觉非常笨拙和肮脏。

我想从对此主题更有经验的人那里获得一些反馈,看看我可以如何改进它。

代码如下,解释如下:

[Fact]
public void ShouldGetItemWithSameId()
{
    var fixture = new Fixture().Customize(new AutoMoqCustomization());
    var facade = fixture.Freeze<Mock<IDataFacade>>();
    facade.Setup(c => c.Get(It.IsAny<int>())).Returns((int i) => new Item { Key = i });

    var sut = fixture.Create<BusinessLogic>();
    var expected = fixture.Create<int>();

    Assert.Equal(expected, sut.Get(expected).Key);
}

我的BusinessLogic 类将IDataFacade 作为构造函数参数,它负责在其Get(int) 方法中检索具有相同ID 的项目,非常基本的东西。

我冻结了 IDataFacade 模拟并设置它以构造一个与 It.IsAny&lt;int&gt; 中的 Id 匹配的对象。然后我创建我的 SUT 并对其进行测试。工作正常。

我想了解是否可以考虑以下方面进行改进:

  • 我必须测试更复杂的方法,例如Query 方法,该方法采用一个包含许多属性的类,这些属性将用作对所查询类型的匹配属性的过滤器。在这种情况下,我不知道如何正确执行模拟的“设置”部分,因为我必须初始化所有或接近所有返回类型的属性,在这种情况下,它不是单个项目,而是整个系列
  • 设置部分感觉不合适,我希望能够在更多方法中重复使用它

我使用TheoryAutoMoqData 进行了一些其他测试,但我无法使用该方法实现此测试(而且我认为是更复杂的测试),所以我切换回使用手动实例化夹具的普通Fact .

任何帮助将不胜感激。

【问题讨论】:

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


【解决方案1】:

总体而言,原始测试看起来不错。以通用方式从测试中提取Stubs and Mocks 的设置既不可能也不容易。

可以做的就是尽量减少测试的安排阶段。这是使用AutoFixture.Xunit自己的单元测试DSL重写的原始测试:

[Theory, TestConventions]
public void ShouldGetItemWithSameId(
    [Frozen]Mock<IDataFacade> facadeStub,
    BusinessLogic sut,
    int expected)
{
    facadeStub
        .Setup(c => c.Get(It.IsAny<int>()))
        .Returns((int i) => new Item { Key = i });

    var result = sut.Get(expected);
    var actual = result.Key;

    Assert.Equal(expected, actual);
}

TestConventions 属性定义为:

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

HTH


示例中使用的示例类型:

public class Item
{
    public int Key { get; set; }
}

public interface IDataFacade
{
    Item Get(int p);
}

public class BusinessLogic
{
    private readonly IDataFacade facade;

    public BusinessLogic(IDataFacade facade)
    {
        this.facade = facade;
    }

    public Item Get(int p)
    {
        return this.facade.Get(p);
    }
}

【讨论】:

  • 这个我喜欢。我会尝试一下并稍后发表评论。谢谢:)
【解决方案2】:

您的测试对我来说看起来不错,但我会建议进行一项更改。如果通过了预期值,则可以收紧以下行以仅返回预期值:

facade.Setup(c => c.Get(It.IsAny<int>())).Returns((int i) => new Item { Key = i });

您只需要移动预期的变量并像这样更改 Is.IsAny:

var expected = fixture.Create<int>();
facade.Setup(c => c.Get(expected)).Returns((int i) => new Item { Key = i });

我必须测试更复杂的方法,例如 Query 方法,它接受一个包含许多属性的类,这些属性将用作被查询类型上匹配属性的过滤器。在这种情况下,我不知道如何正确执行模拟的“设置”部分,因为我必须初始化所有或接近所有返回类型的属性,在这种情况下,它不是单个项目,而是一个完整的集合

我认为您不需要初始化返回类型的所有值。我猜你的 DataFacade 返回一个对象(或在这种情况下的列表)?您需要做的就是确保返回的对象与从 DataFacade 返回的对象的引用相匹配,您无需担心属性等,因为您没有测试这些对象的构造,只是它们是回来。如果我误解了,而您正在 BusinessLogic 中构建对象,那就另当别论了。就个人而言,我不会让业务逻辑依赖于数据层,但这是一个不同的讨论。 :-)

设置部分感觉不合适,我希望能够在更多方法中重复使用它

你可以。将其提取到单独的方法中,或者如果它适用于类中的每个测试,则将其放入 setup 方法中。我不熟悉 XUnit,但我使用过的所有其他测试框架都提供了进行通用设置的能力,所以我怀疑 XUnit 会有所不同。

我最后的评论是,像对待生产代码一样对待你的测试代码,如果它看起来一团糟,做一些事情让它变得更好。测试非常适合描述系统的行为,但如果它们难以阅读(和维护),您将失去很多价值。

编辑,原来这不是我的最终评论!如果您是 TDD 的新手,我不确定您是否是新手,请不要陷入测试应用程序中每个类的陷阱,这是一种流行的常见模式,在我看来它贬低了 TDD .我已经写了blog post on my feelings,Ian Cooper 就此事给了superb presentation

【讨论】:

  • AutoFixture 的 create() 的一个不错的小包装是 tdd-toolkit,所以 var expected = fixture.Create&lt;int&gt;(); 变为 var expected = Any.Integer();
  • @robi-y 包装器有点牵强;虽然我确定它有它的位置,但它并没有解决 OP 想要的一堆东西,所以为什么要引入两组语法/约定的混淆。
  • @RubenBartelink 普遍同意,但在某些情况下,它帮助我实现了更清晰的代码,也许一个更好的例子是,例如 Any.IntegerOtherThan(42) 当然不需要她 -只是建议一些有时会有所帮助的微小语法糖,谢谢
  • 来自 AF 的 @robi-y Generator&lt;int&gt;().First(x=&gt;x!=42) 非常适合我。一个新的库通常以 -100 分开始。
【解决方案3】:

一些基础知识:

您的测试类在每个单独的测试运行之前被实例化(并调用它的构造函数)。例如如果您的 Test 类具有三个具有 [Fact] 属性的方法,它会被实例化 3 次

TestFixture 类是另一个类,旨在为您的测试类中的所有测试一次性实例化。

要完成这项工作,您的测试类必须实现 IUseFixture 接口,例如实现一个成员 SetFixture()

您可以为多个测试类使用相同的 MyTestFixture 类。

您可以在 TestFixture 中进行所有的模拟设置。

这里是总体布局:

public class MyTestFixture
{    
    public Mock<MyManager> ManagerMock;

    public TestFixture() // runs once
    {
        ManagerMock.Setup(...);
    }
}

public MyTestClass : IUseFixture<MyTestFixture>
{
    private MyTestFixture fixture;

    public MyTestClass()
    {
         // ctor runs for each [Fact]
    }

    public void SetFixture(MyTestFixture fixture)
    {
        this.fixture = fixture;
    }

    [Fact]
    public void MyTest
    {
         // use Mock
         fixture.ManagerMock.DoSomething()
    }
}

【讨论】:

  • 我已经知道所有这些东西,但我认为使用 IUseFixture 接口有点违背了使用 AutoFixture 的目的。不过我可能错了。
  • “在 TestFixture 中,你要做所有的 Mock-Setups”——我不同意,你应该只做任何适用于该夹具中所有测试的设置。
  • @DoctorMick 是的,当然,在 TestFixture 中,您(仅)执行所有测试所需的所有操作
  • @DrKoch 并不是说​​ xUnit v2 有更简洁的方式来管理固定装置(即没有 IUseFixture,只是 ctor 注入)
猜你喜欢
  • 2019-09-03
  • 2014-01-24
  • 2022-12-22
  • 1970-01-01
  • 2020-02-18
  • 2021-02-12
  • 2013-11-14
  • 2011-06-25
  • 2017-01-22
相关资源
最近更新 更多