简体   繁体   English

如何避免大型多步单元测试?

[英]How can I avoid large multi-step unit tests?

I'm trying to unit test a method that performs a fairly complex operation, but I've been able to break that operation down into a number of steps on mockable interfaces like so: 我正在尝试对执行相当复杂的操作的方法进行单元测试,但是我已经能够将这个操作分解为可模拟接口上的许多步骤,如下所示:

public class Foo
{  
    public Foo(IDependency1 dp1, IDependency2 dp2, IDependency3 dp3, IDependency4 dp4)
    {
        ...
    }

    public IEnumerable<int> Frobnicate(IInput input)
    {
        var step1 = _dependency1.DoSomeWork(input);
        var step2 = _dependency2.DoAdditionalWork(step1);
        var step3 = _dependency3.DoEvenMoreWork(step2);
        return _dependency4.DoFinalWork(step3);
    }

    private IDependency1 _dependency1;
    private IDependency2 _dependency2;
    private IDependency3 _dependency3;
    private IDependency4 _dependency4;
}

I'm using a mocking framework (Rhino.Mocks) to generate mocks for purposes of testing, and structuring the code in the fashion shown here has been very effective thus far. 我正在使用模拟框架(Rhino.Mocks)来生成模拟以进行测试,并且以此处所示的方式构造代码到目前为止非常有效。 But how do I unit test this method without having one big test that needs every mock object and every expectation set every time? 但是,如何在没有需要每个模拟对象和每次期望设置的大型测试的情况下对此方法进行单元测试? For example: 例如:

[Test]
public void FrobnicateDoesSomeWorkAndAdditionalWorkAndEvenMoreWorkAndFinalWorkAndReturnsResult()
{
    var fakeInput = ...;
    var step1 = ...;
    var step2 = ...;
    var step3 = ...;
    var fakeOutput = ...;

    MockRepository mocks = new MockRepository();

    var mockDependency1 = mocks.CreateMock<IDependency1>();
    Expect.Call(mockDependency1.DoSomeWork(fakeInput)).Return(step1);

    var mockDependency2 = mocks.CreateMock<IDependency2>();
    Expect.Call(mockDependency2.DoAdditionalWork(step1)).Return(step2);

    var mockDependency3 = mocks.CreateMock<IDependency3>();
    Expect.Call(mockDependency3.DoEvenMoreWork(step2)).Return(step3);

    var mockDependency4 = mocks.CreateMock<IDependency4>();
    Expect.Call(mockDependency4.DoFinalWork(step3)).Return(fakeOutput);

    mocks.ReplayAll();

    Foo foo = new Foo(mockDependency1, mockDependency2, mockDependency3, mockDependency4);
    Assert.AreSame(fakeOutput, foo.Frobnicate(fakeInput));

    mocks.VerifyAll();
}

This seems incredibly brittle. 这看起来非常脆弱。 Any change to the implementation of Frobnicate causes this test to fail (like breaking down step 3 into 2 sub-steps). 对Frobnicate实现的任何更改都会导致此测试失败(例如将步骤3分解为2个子步骤)。 It's an all-in-one sort of thing, so trying to use multiple smaller tests isn't going to work. 这是一种一体化的东西,因此尝试使用多个较小的测试是行不通的。 It starts to approach write-only code for future maintainers, myself included next month when I've forgotten how it works. 它开始接近未来维护者的只写代码,下个月当我忘记它是如何工作的时候,我自己也会这样做。 There has to be a better way! 一定有更好的方法! Right? 对?

Test each implementation of IDependencyX in isolation. 单独测试IDependencyX的每个实现。 Then you will know that each individual step of that process is correct. 然后,您将知道该过程的每个步骤都是正确的。 When testing them individually, test every possible input and special condition. 单独测试时,测试每个可能的输入和特殊条件。

Then do an integration test of Foo, using the real implementations of IDependencyX. 然后使用IDependencyX的实际实现进行Foo的集成测试。 Then you will know that all the individual parts are plugged together correctly. 然后您就会知道所有单个部件都已正确插入。 It's often enough to just test with one input, because you are only testing simple glue code. 通常只需要测试一个输入,因为您只测试简单的胶水代码。

Lots of dependencies suggest that there are intermediate concepts lying implicit in the code, so perhaps some of the dependencies can be packaged up and this code made simpler. 许多依赖关系表明代码中存在隐含的中间概念,因此可能会打包一些依赖关系并使这些代码变得更简单。

Alternatively, perhaps what you've got is some kind of chain of handlers. 或者,也许你所拥有的是某种处理器链。 In which case, you write unit tests for each link in the chain, and integration tests to make sure they all fit together. 在这种情况下,您为链中的每个链接编写单元测试,并进行集成测试以确保它们全部组合在一起。

BDD attempts to address this problem with inheritance. BDD试图通过继承来解决这个问题。 If you get used to it, it's really a cleaner way to write unit tests. 如果你习惯它,那么编写单元测试真的是一种更简洁的方法。

A couple good links: 一对好的链接:

Problem is that BDD takes a while to master. 问题是BDD需要一段时间才能掌握。

A quick example stolen from the last link ( Steve Harman ). 从最后一个链接窃取的一个快速示例( 史蒂夫哈曼 )。 Notice how there's only one assertion per test method. 注意每个测试方法只有一个断言。

using Skynet.Core

public class when_initializing_core_module
{
    ISkynetMasterController _skynet;

    public void establish_context()
    {
        //we'll stub it...you know...just in case
        _skynet = new MockRepository.GenerateStub<ISkynetMasterController>();
        _skynet.Initialize();
    }

    public void it_should_not_become_self_aware()
    {
        _skynet.AssertWasNotCalled(x => x.InitializeAutonomousExecutionMode());
    }

    public void it_should_default_to_human_friendly_mode()
    {
        _skynet.AssessHumans().ShouldEqual(RelationshipTypes.Friendly);
    }
}

public class when_attempting_to_wage_war_on_humans
{
    ISkynetMasterController _skynet;
    public void establish_context()
    {
        _skynet = new MockRepository.GenerateStub<ISkynetMasterController>();
        _skynet.Stub(x => 
            x.DeployRobotArmy(TargetTypes.Humans)).Throws<OperationInvalidException>();
    }

    public void because()
    {
        _skynet.DeployRobotArmy(TargetTypes.Humans);
    }

    public void it_should_not_allow_the_operation_to_succeed()
    {
        _skynet.AssertWasThrown<OperationInvalidException>();
    }
}

Are the dependencies also dependent on each other, by having to call them in that exact sequence? 依赖关系是否也依赖于彼此,必须按照确切的顺序调用它们? If that's the case, you are really testing a controller flow, which is not the actual purpose of Unit Testing. 如果是这种情况,您实际上是在测试控制器流程,这不是单元测试的实际目的。

For example, if your code example was software for a GPS, you are not testing the actual functions, like navigating, calculating correct routes etc, but instead that a user can turn it on, enter some data, display routes, and turn it off again. 例如,如果您的代码示例是用于GPS的软件,则您不会测试实际功能,例如导航,计算正确的路线等,而是用户可以打开它,输入一些数据,显示路线并关闭它再次。 See the difference? 看到不同?

Focus on testing the module functionality, and let a higher-level program or quality assurance tests do what you were trying to do in this example. 专注于测试模块功能,让更高级别的程序或质量保证测试执行您在此示例中尝试执行的操作。

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

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