3

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

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 个子步骤)。这是一种一体化的东西,因此尝试使用多个较小的测试是行不通的。它开始为未来的维护者提供只写代码,下个月我也忘记了它是如何工作的。一定有更好的方法!对?

4

4 回答 4

5

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

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

于 2009-02-26T20:00:57.763 回答
1

许多依赖项表明代码中隐含了中间概念,因此也许可以将一些依赖项打包起来,使代码更简单。

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

于 2009-05-21T18:40:46.680 回答
0

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>();
    }
}
于 2009-02-26T19:59:22.947 回答
0

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

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

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

于 2009-02-26T20:19:51.213 回答