XUnit,AutoFixture和Moq最佳实践

时间:2014-12-18 09:35:42

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

我正在阅读大量文档和示例,了解如何正确地对标题中的三个组件进行单元测试。我想出了一种针对我的业务逻辑的方法的测试方法,但它感觉非常笨重和肮脏。

我希望从这个主题更有经验的人那里获得一些反馈,看看我如何改进它。

以下是代码,解释如下:

[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<int>中的Id匹配的对象。然后我创建我的SUT并测试它。工作正常。

我想了解以下是否可以改进:

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

我使用Theory使用AutoMoqData进行了一些其他测试,但我无法使用该方法实现此测试(我认为更复杂的测试),因此我切换回普通{{1手动实例化夹具。

非常感谢任何帮助。

3 个答案:

答案 0 :(得分:8)

总体而言,原始测试看起来不错。以通用的方式从测试中提取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);
    }
}

答案 1 :(得分:5)

你的测试对我来说很好,虽然我会推荐一个改变。如果传递了预期值,则可以收紧以下行以仅返回预期值:

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中的对象,那么这是另一回事。就个人而言,我不会将业务逻辑依赖于数据层,但这是一个不同的讨论。 : - )

  

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

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

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

编辑,结果证明这不是我最后的评论!如果你是TDD的新手,我不确定你是不是,不要陷入测试应用程序中每个类的陷阱,这是一种普遍的模式已经变得普遍并且在我看来它贬值了TDD 。我写了一篇blog post on my feelings,Ian Cooper就这件事给了superb presentation

答案 2 :(得分:2)

一些基础知识:

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

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()
    }
}