简体   繁体   English

XUnit,AutoFixture和Moq最佳实践

[英]XUnit, AutoFixture and Moq best practice

I'm reading a lot of documentation and examples about how to properly unit test things combining the three components in the title. 我正在阅读大量文档和示例,了解如何正确地对标题中的三个组件进行单元测试。 I came up with a test method for a method on my business logic, but it feels very clunky and dirty. 我想出了一种针对我的业务逻辑的方法的测试方法,但它感觉非常笨重和肮脏。

I'd like to get some feedback from people more experienced on this topic to see how I can improve it. 我想从那些对这个主题更有经验的人那里得到一些反馈,看看我如何改进它。

Here's the code, explanation follows: 这是代码,解释如下:

[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);
}

My BusinessLogic class takes an IDataFacade as constructor parameter, which is responsible in its Get(int) method to retrieve the item with the same Id, pretty basic stuff. 我的BusinessLogic类将IDataFacade作为构造函数参数,它在其Get(int)方法中负责检索具有相同Id,相当基本的东西的项目。

I freeze the IDataFacade mock and I set it up to construct an object matching the Id in It.IsAny<int> . 我冻结了IDataFacade ,并将其设置为构造一个与It.IsAny<int>的Id匹配的对象。 I then create my SUT and test it. 然后我创建我的SUT并测试它。 Works fine. 工作良好。

I'd like to understand if I can improve things considering the following: 我想了解一下,考虑到以下方面我是否可以改进:

  • I have to test more complex methods, like a Query method that takes a class containing a lot of properties that will be used as filters on matching properties on the type being queried. 我必须测试更复杂的方法,比如一个Query方法,它接受一个包含很多属性的类,这些属性将用作匹配被查询类型的属性的过滤器。 In this case I wouldn't know how to properly do the "Setup" part of the mock, since I have to initialize all, or close to all, the properties of the returned type, and in this scenario it's not a single Item but a whole collection 在这种情况下,我不知道如何正确地执行模拟的“设置”部分,因为我必须初始化所有或接近所有返回类型的属性,并且在这种情况下它不是单个项目但是整个系列
  • The setup part feels out of place, I'd like to be able to reuse it in more methods 设置部分感觉不合适,我希望能够在更多方法中重复使用它

I have some other tests using Theory with AutoMoqData but I was unable to achieve this test (and I think the more complex ones) using that approach, so I switched back to plain Fact with manually instantiated fixture. 我使用TheoryAutoMoqData进行了一些其他的测试但是我无法使用这种方法实现这个测试(我认为更复杂的测试),所以我用手动实例化的夹具切换回普通的Fact

Any help will be extremely appreciated. 任何帮助将非常感激。

Overall, the original test looks good. 总的来说,原始测试看起来不错。 It's not possible nor easy to extract the setup of Stubs and Mocks out of the test, in a generic fashion. 以通用的方式从测试中提取Stubs和Mocks的设置是不可能也不容易的。

What you can do though, is minimize the Arrange phase of the test. 但是,您可以做的是最小化测试的排列阶段。 Here's the original test re-written using AutoFixture.Xunit 's own unit-testing DSL: 这是使用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);
}

The TestConventions attribute is defined as: TestConventions属性定义为:

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

HTH HTH


Sample types used in the example: 示例中使用的示例类型:

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

Your test looks fine to me, although I would recommend one change. 你的测试对我来说很好,虽然我会推荐一个改变。 The following line could be tightened up to only return the expected value if the expected value is passed: 如果传递了预期值,则可以收紧以下行以仅返回预期值:

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

All you'd need to do it move the expected variable and change the Is.IsAny like so: 所有你需要做的就是移动预期变量并像这样更改Is.IsAny:

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

I have to test more complex methods, like a Query method that takes a class containing a lot of properties that will be used as filters on matching properties on the type being queried. 我必须测试更复杂的方法,比如一个Query方法,它接受一个包含很多属性的类,这些属性将用作匹配被查询类型的属性的过滤器。 In this case I wouldn't know how to properly do the "Setup" part of the mock, since I have to initialize all, or close to all, the properties of the returned type, and in this scenario it's not a single Item but a whole collection 在这种情况下,我不知道如何正确地执行模拟的“设置”部分,因为我必须初始化所有或接近所有返回类型的属性,并且在这种情况下它不是单个项目但是整个系列

I don't think you would need to initialise all of the values on the returned type. 我认为您不需要初始化返回类型的所有值。 I'm guessing your DataFacade returns an object (or list of in this case)? 我猜你的DataFacade返回一个对象(或在这种情况下的列表)? All you'd need to do is make sure the objects returned match the references of those returned from the DataFacade, you don't need to worry about properties etc as you're not testing the construction of those objects, just that they're returned. 您需要做的就是确保返回的对象与从DataFacade返回的对象的引用相匹配,您不需要担心属性等,因为您没有测试这些对象的构造,只是它们是回来。 If I've misunderstood and you're constructing the objects in the BusinessLogic then that's a different matter. 如果我误解了你并且你正在构建BusinessLogic中的对象,那么这是另一回事。 Personally, I wouldn't have the business logic dependent on the data layer but that's a different discussion. 就个人而言,我不会将业务逻辑依赖于数据层,但这是一个不同的讨论。 :-) :-)

The setup part feels out of place, I'd like to be able to reuse it in more methods 设置部分感觉不合适,我希望能够在更多方法中重复使用它

You can. 您可以。 Either extract it out to a separate method or, if it is applicable to every test in the class, put it in a setup method. 将其提取到单独的方法中,或者,如果它适用于类中的每个测试,则将其放入设置方法中。 I'm not familiar with XUnit but every other test framework I've used provides the ability to do common setup so I doubt XUnit will be any different. 我不熟悉XUnit,但我使用的每个其他测试框架都提供了进行常见设置的能力,所以我怀疑XUnit会有什么不同。

And my final comment, treat your test code as you would treat your production code, if it looks a mess do things to make it better. 我最后的评论,就像对待你的生产代码一样对待你的测试代码,如果它看起来很乱,那就做一些让它变得更好的东西。 Tests are great for describing the behavior of a system but if they're difficult to read (and maintain) you lose a lot of value. 测试非常适合描述系统的行为,但如果它们难以阅读(和维护),则会失去很多价值。

Edit, turns out that isn't my final comment! 编辑,结果证明这不是我最后的评论! If you're new to TDD, which I'm not sure you are, don't fall in to the trap of testing each and every class in your application, it's a common pattern that has become prevalent and it devalues TDD in my opinion. 如果你是TDD的新手,我不确定你是不是,不要陷入测试应用程序中每个类的陷阱,这是一种普遍的模式已经变得普遍并且在我看来它贬值了TDD 。 I've wrote a blog post on my feelings and Ian Cooper has given a superb presentation on the matter. 我写了一篇关于我的感受博客文章 ,Ian Cooper在这个问题上发表精彩的演讲

Some basics: 一些基础知识:

Your Test Class is instantiated (and its constructor called) before each single Test is run. 在运行每个单独的Test之前,将对Test Class进行实例化(并调用其构造函数)。 eg if your Test class has three methods with [Fact] attribute, it gets instantiated three times 例如,如果您的Test类有三个带[Fact]属性的方法,它将被实例化三次

A TestFixture class is another class which is meant to be instanciated a single time for all the Tests in your test class. TestFixture类是另一个类,它意味着一次性为测试类中的所有测试实例化。

To make this work, your test class must implement the IUseFixture interface, eg implement a member SetFixture() 要使这项工作,您的测试类必须实现IUseFixture接口,例如实现成员SetFixture()

You may use the same MyTestFixture class for several Test classes. 您可以为多个Test类使用相同的MyTestFixture类。

Inside the TestFixture you do all the Mock-Setups. 在TestFixture内部,您可以执行所有模拟设置。

Here the general layout: 这里的总体布局:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM