【问题标题】:How can I avoid large multi-step unit tests?如何避免大型多步骤单元测试?
【发布时间】:2009-02-26 19:51:00
【问题描述】:

我正在尝试对执行相当复杂操作的方法进行单元测试,但我已经能够将该操作分解为可模拟接口上的多个步骤,如下所示:

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

我正在使用一个模拟框架 (Rhino.Mocks) 来生成模拟以进行测试,到目前为止,按照此处所示的方式构建代码非常有效。但是,我如何对这种方法进行单元测试,而不需要每次都设置每个模拟对象和每个期望的大型测试呢?例如:

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

这看起来非常脆弱。对 Frobnicate 实现的任何更改都会导致此测试失败(例如将步骤 3 分解为 2 个子步骤)。这是一种一体化的东西,因此尝试使用多个较小的测试是行不通的。它开始为未来的维护者提供只写代码,下个月我也忘记了它是如何工作的。一定有更好的方法!对吧?

【问题讨论】:

    标签: c# unit-testing inversion-of-control mocking rhino-mocks


    【解决方案1】:

    单独测试 IDependencyX 的每个实现。然后,您将知道该过程的每个单独步骤都是正确的。单独测试它们时,测试每个可能的输入和特殊条件。

    然后使用 IDependencyX 的真实实现对 Foo 进行集成测试。然后你就会知道所有的单独的部分都正确地插在一起了。通常只测试一个输入就足够了,因为您只是在测试简单的胶水代码。

    【讨论】:

      【解决方案2】:

      大量的依赖表明代码中隐含了中间概念,所以也许可以将一些依赖打包起来,让这段代码更简单。

      或者,也许您拥有的是某种处理程序链。在这种情况下,您需要为链中的每个环节编写单元测试,并编写集成测试以确保它们都适合。

      【讨论】:

        【解决方案3】:

        BDD 试图通过继承来解决这个问题。如果你习惯了,它确实是一种更简洁的编写单元测试的方式。

        几个不错的链接:

        问题是 BDD 需要一段时间才能掌握。

        从最后一个链接 (Steve Harman) 中窃取的一个简单示例。注意每个测试方法只有一个断言。

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

        【讨论】:

        • 那个例子似乎只测试了一个存根?
        • 这个例子是为了好玩。 :) 我认为,它确实抓住了 BDD 的精髓,而没有过多的细节。学习 BDD 的问题在于,一次要深入了解很多东西,所以很难找到一个足够简单的例子来传达 BDD 的所有优点,但又不太复杂。
        • Michael,感谢您对 BDD 的指点!好东西,我已经转换了一个测试套件。
        【解决方案4】:

        依赖关系是否也相互依赖,必须按照确切的顺序调用它们?如果是这种情况,您实际上是在测试控制器流,这不是单元测试的实际目的。

        例如,如果您的代码示例是 GPS 软件,那么您不是在测试实际功能,例如导航、计算正确路线等,而是用户可以打开它、输入一些数据、显示路线和再次将其关闭。看出区别了吗?

        专注于测试模块功能,让更高级别的程序或质量保证测试完成您在本示例中尝试执行的操作。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2020-01-18
          • 2016-11-22
          • 1970-01-01
          • 2012-04-19
          • 1970-01-01
          • 1970-01-01
          • 2018-12-19
          相关资源
          最近更新 更多