TDD和DI:依赖注入变得麻烦

时间:2009-03-03 15:22:49

标签: tdd dependency-injection

C#,nUnit和Rhino Mocks,如果证明适用的话。

我对TDD的追求仍在继续,因为我试图围绕一个复杂的函数进行测试。假设我正在编写一个表单,当保存时,还必须保存表单中的依赖对象...形成问题的答案,附件(如果可用)和“日志”条目(例如“blahblah更新表单”)或“blahblah附上了一个档案。”)。此保存功能还会根据在保存功能期间表单状态的更改方式向各种人发送电子邮件。

这意味着为了完全测试表单的save函数及其所有依赖项,我必须注入五到六个数据提供程序来测试这一个函数,并确保以正确的方式和顺序启动所有内容。在为表单对象编写多个链式构造函数以插入模拟的提供程序时,这很麻烦。我想我错过了一些东西,无论是重构方式还是更好的方式来设置模拟数据提供者。

我是否应该进一步研究重构方法以了解如何简化此功能?观察者模式是如何发声的,以便依赖对象检测父表单何时被保存并自行处理?我知道有人说要拆分函数以便可以测试...这意味着我测试了每个依赖对象的单独保存函数,而不是表单本身的保存函数,它决定了每个函数应该如何保存自己。第一名?

8 个答案:

答案 0 :(得分:16)

首先,如果您正在关注TDD,那么您不会围绕复杂的函数进行测试。您可以围绕测试包装函数。实际上,即使这是不对的。您将测试和功能交织在一起,几乎完全同时编写,测试只是稍微提前一点。见The Three Laws of TDD

当你遵循这三条法律并且勤于重构时,你就永远不会遇到“复杂的功能”。相反,你最终会得到许多经过测试的简单功能。

现在,谈谈你的观点。如果你已经有了“一个复杂的功能”而你想围绕它进行测试,那么你应该:

  1. 明确添加您的模拟,而不是通过DI。 (例如像'test'标志和'if'语句那样可怕的选择mocks而不是真实对象的东西。
  2. 编写一些测试以涵盖组件的基本操作。
  3. 无情地重构,将复杂的功能分解成许多简单的小功能,同时尽可能多地运行拼凑测试。
  4. 将'test'标志尽可能高。在重构时,将数据源传递给小的简单函数。除了最顶层的功能之外,不要让'test'标志感染任何标志。
  5. 重写测试。在重构时,重写尽可能多的测试来调用简单的小函数而不是大的顶级函数。您可以通过测试将模拟传递给简单的函数。
  6. 摆脱'测试'标志并确定你真正需要多少DI。由于您可以在较低级别编写可以通过参数插入模拟的测试,因此您可能不再需要在顶层模拟许多数据源。
  7. 如果在此之后,DI仍然很麻烦,那么考虑注入一个包含所有数据源引用的对象。注入一件事而不是很多东西总是比较容易。

答案 1 :(得分:7)

使用AutoMocking容器。有一个是为RhinoMocks编写的。

想象一下,你有一个通过构造函数注入注入大量依赖项的类。以下是使用RhinoMocks设置它的样子,没有AutoMocking容器:

private MockRepository _mocks;
private BroadcastListViewPresenter _presenter;
private IBroadcastListView _view;
private IAddNewBroadcastEventBroker _addNewBroadcastEventBroker;
private IBroadcastService _broadcastService;
private IChannelService _channelService;
private IDeviceService _deviceService;
private IDialogFactory _dialogFactory;
private IMessageBoxService _messageBoxService;
private ITouchScreenService _touchScreenService;
private IDeviceBroadcastFactory _deviceBroadcastFactory;
private IFileBroadcastFactory _fileBroadcastFactory;
private IBroadcastServiceCallback _broadcastServiceCallback;
private IChannelServiceCallback _channelServiceCallback;

[SetUp]
public void SetUp()
{
    _mocks = new MockRepository();
    _view = _mocks.DynamicMock<IBroadcastListView>();

    _addNewBroadcastEventBroker = _mocks.DynamicMock<IAddNewBroadcastEventBroker>();

    _broadcastService = _mocks.DynamicMock<IBroadcastService>();
    _channelService = _mocks.DynamicMock<IChannelService>();
    _deviceService = _mocks.DynamicMock<IDeviceService>();
    _dialogFactory = _mocks.DynamicMock<IDialogFactory>();
    _messageBoxService = _mocks.DynamicMock<IMessageBoxService>();
    _touchScreenService = _mocks.DynamicMock<ITouchScreenService>();
    _deviceBroadcastFactory = _mocks.DynamicMock<IDeviceBroadcastFactory>();
    _fileBroadcastFactory = _mocks.DynamicMock<IFileBroadcastFactory>();
    _broadcastServiceCallback = _mocks.DynamicMock<IBroadcastServiceCallback>();
    _channelServiceCallback = _mocks.DynamicMock<IChannelServiceCallback>();


    _presenter = new BroadcastListViewPresenter(
        _addNewBroadcastEventBroker,
        _broadcastService,
        _channelService,
        _deviceService,
        _dialogFactory,
        _messageBoxService,
        _touchScreenService,
        _deviceBroadcastFactory,
        _fileBroadcastFactory,
        _broadcastServiceCallback,
        _channelServiceCallback);

    _presenter.View = _view;
}

现在,AutoMocking容器的内容与此相同:

private MockRepository _mocks;
private AutoMockingContainer _container;
private BroadcastListViewPresenter _presenter;
private IBroadcastListView _view;

[SetUp]
public void SetUp()
{

    _mocks = new MockRepository();
    _container = new AutoMockingContainer(_mocks);
    _container.Initialize();

    _view = _mocks.DynamicMock<IBroadcastListView>();
    _presenter = _container.Create<BroadcastListViewPresenter>();
    _presenter.View = _view;

}

更容易,是吗?

AutoMocking容器会自动为构造函数中的每个依赖项创建模拟,您可以像以下那样访问它们进行测试:

using (_mocks.Record())
    {
      _container.Get<IChannelService>().Expect(cs => cs.ChannelIsBroadcasting(channel)).Return(false);
      _container.Get<IBroadcastService>().Expect(bs => bs.Start(8));
    }

希望有所帮助。我知道随着AutoMocking容器的出现,我的测试寿命变得更加轻松。

答案 2 :(得分:5)

你说得对,这很麻烦。

模拟方法的支持者会指出代码写得不正确。也就是说,您不应该在此方法中构造依赖对象。相反,注入API应该具有创建适当对象的函数。

至于模拟6个不同的对象,这是真的。但是,如果您还对那些系统进行单元测试,则这些对象应该已经具有可以使用的模拟基础结构。

最后,使用一个模拟框架为您完成一些工作。

答案 3 :(得分:5)

我没有你的代码,但我的第一反应是你的测试试图告诉你你的对象有太多的协作者。在这样的情况下,我总是发现那里有一个缺失的构造应该被打包成更高级别的结构。使用自动锁定容器只会掩盖您从测试中获得的反馈。有关详细讨论,请参阅http://www.mockobjects.com/2007/04/test-smell-bloated-constructor.html

答案 4 :(得分:4)

在这种情况下,我通常会发现“这表明你的对象有太多的依赖关系”或“你的对象有太多的合作者”的陈述是一个相当似是而非的主张。当然,MVC控制器或表单将调用许多不同的服务和对象来履行其职责;毕竟,它是坐在应用程序的顶层。您可以将这些依赖项中的一些一起放入更高级别的对象(比如,一个ShippingMethodRepository和一个TransitTimeCalculator结合到一个ShippingRateFinder中),但这只是到目前为止,特别是对于这些顶级的,面向表示的对象。这是一个较少的模拟对象,但你只是通过一个间接层来模糊实际的依赖,而不是实际删除它们。

一个亵渎性的建议就是说,如果你是依赖注入一个对象并为它创建一个不太可能改变的接口(你真的会在改变你的代码时放入一个新的MessageBoxService吗?真的吗? ),然后不要打扰。该依赖项是对象预期行为的一部分,您应该只测试它们,因为集成测试是真正的业务价值所在。

另一个亵渎性的建议是,我通常在单元测试MVC控制器或Windows窗体中看不到什么用处。每次我看到有人嘲笑HttpContext并测试是否设置了cookie,我想尖叫。谁关心AccountController是否设置了cookie?我不。 cookie与将控制器视为黑盒子无关;集成测试是测试其功能所需要的(嗯,在集成测试中对Login()后对PrivilegedArea()的调用失败)。这样,如果登录cookie的格式发生变化,您就可以避免使一百万次无用的单元测试无效。

保存对象模型的单元测试,保存表示层的集成测试,并在可能的情况下避免使用模拟对象。如果嘲笑某个特定的依赖很难,那么是时候务实了:只是不要进行单元测试并编写集成测试而不要浪费你的时间。

答案 5 :(得分:3)

简单的答案是,您尝试测试的代码做得太多。我认为坚持Single Responsibility Principle可能有所帮助。

“保存”按钮方法应该只包含将事物委托给其他对象的顶级调用。然后可以通过接口抽象这些对象。然后,当您测试“保存”按钮方法时,只测试与模拟对象的交互。

下一步是将测试写入这些较低级别的类,但事情应该变得更容易,因为您只是单独测试它们。如果您需要复杂的测试设置代码,这可以很好地指示设计不良(或者测试方法不好)。

推荐阅读:

  1. Clean Code: A Handbook of Agile Software Craftsmanship
  2. Google's guide to writing testable code

答案 6 :(得分:1)

构造函数DI不是进行DI的唯一方法。由于您使用的是C#,如果您的构造函数没有重要的工作,您可以使用Property DI。这会以对象的构造函数为代价,以牺牲函数的复杂性为代价来大大简化事情。您的函数必须检查任何依赖属性的无效性,并在它们开始工作之前抛出InvalidOperation(如果它们为null)。

答案 7 :(得分:0)

当很难测试某些东西时,通常是代码质量的症状,代码是不可测试的(在this podcast,IIRC中提到)。建议重构代码,以便代码易于测试。决定如何将代码拆分为类的一些启发式方法是SRP and OCP。有关更具体的说明,有必要查看相关代码。