如何避免大型多步骤单元测试?
我试图单元测试执行一个相当复杂的操作的方法,但我已经能够向下打破运行到了一些关于mockable接口的步骤如下所示:如何避免大型多步骤单元测试?
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个子步骤)。这是一种一体化的方式,所以尝试使用多个较小的测试是行不通的。它开始为未来的维护人员提供只写代码,下个月我已经忘记了它是如何工作的。一定有更好的方法!对?
单独测试IDependencyX的每个实现。然后你会知道该过程的每个步骤都是正确的。单独测试时,请测试每种可能的输入和特殊情况。
然后使用IDependencyX的真实实现对Foo进行集成测试。然后你会知道所有的单个部件都被正确地插在一起。仅用一个输入进行测试就足够了,因为您只是测试简单的胶水代码。
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>();
}
}
依赖关系是否也相互依赖,必须按照确切的顺序调用它们?如果是这样的话,你真的在测试一个控制器流程,这不是单元测试的实际目的。例如,如果您的代码示例是GPS软件,那么您并未测试实际功能,如导航,计算正确的路线等,而是用户可以打开它,输入一些数据,显示路线,并再次关闭它。看到不同?
专注于测试模块功能,并让更高级别的程序或质量保证测试完成您在本示例中尝试执行的操作。
大量的依赖关系表明存在隐含在代码中的中间概念,所以也许可以打包一些依赖关系,并且使代码变得更简单。
另外,也许你得到的是一些处理程序链。在这种情况下,您会为链中的每个链接编写单元测试,并进行集成测试以确保它们全部合在一起。
该示例似乎只测试一个存根? – 2009-02-26 20:19:53