TDD и DI: инъекции зависимостей становятся громоздкими - PullRequest
9 голосов
/ 03 марта 2009

C #, nUnit и Rhino Mocks, если это применимо.

Мой квест с TDD продолжается, когда я пытаюсь обернуть тесты вокруг сложной функции. Допустим, я кодирую форму, которая при сохранении должна также сохранять зависимые объекты внутри формы ... ответы на вопросы формы, вложения, если они доступны, и записи в «журнале» (например, «бла-бла обновил форму». "блабла приложил файл.") Эта функция сохранения также отправляет электронные письма различным людям в зависимости от того, как изменилось состояние формы во время функции сохранения.

Это означает, что для того, чтобы полностью протестировать функцию сохранения формы со всеми ее зависимостями, мне нужно ввести пять или шесть провайдеров данных, чтобы протестировать эту функцию и убедиться, что все сработало в правильном порядке и порядке. Это громоздко, когда пишется несколько цепочечных конструкторов для объекта формы для вставки фиктивных провайдеров. Я думаю, что я что-то упустил, либо из-за рефакторинга, либо просто из-за того, что лучше настроить провайдеров поддельных данных.

Должен ли я продолжить изучение методов рефакторинга, чтобы увидеть, как можно упростить эту функцию? Как звучит шаблон наблюдателя, так что зависимые объекты обнаруживают, когда родительская форма сохраняется, и обрабатывают себя? Я знаю, что люди говорят разделить функцию, чтобы ее можно было протестировать ... то есть я тестирую отдельные функции сохранения каждого зависимого объекта, но не функцию сохранения самой формы, которая диктует, как каждый должен сохранять себя в первое место?

Ответы [ 8 ]

16 голосов
/ 06 марта 2009

Во-первых, если вы следуете TDD, тогда вы не оборачиваете тесты сложной задачей. Вы оборачиваете функцию вокруг ваших тестов. На самом деле, даже это не правильно. Вы переплетаете свои тесты и функции, записывая их практически в одно и то же время, причем тесты немного опережают функции. См. Три закона TDD .

Когда вы следуете этим трем законам и усердно относитесь к рефакторингу, вы никогда не получите «сложную функцию». Скорее вы получите множество проверенных простых функций.

Теперь перейдем к вашей точке. Если у вас уже есть «сложная функция» и вы хотите обернуть тесты вокруг нее, вам следует:

  1. Добавляйте свои макеты явно, а не через DI. (например, что-то ужасное, например, флаг 'test' и оператор 'if', который выбирает макеты вместо реальных объектов).
  2. Напишите несколько тестов, чтобы охватить основные операции компонента.
  3. Реорганизуйте беспощадно, разбивая сложную функцию на множество маленьких простых функций, одновременно выполняя ваши собранные вместе тесты как можно чаще.
  4. Нажмите флаг «Тест» как можно выше. При рефакторинге передайте источники данных небольшим простым функциям. Не позволяйте флагу 'test' заражать любую функцию, кроме самой верхней.
  5. Переписать тесты. В процессе рефакторинга перепишите как можно больше тестов, чтобы вызывать простые маленькие функции вместо большой функции верхнего уровня. Вы можете передать свои макеты в простые функции из ваших тестов.
  6. Избавьтесь от флага 'test' и определите, сколько DI вам действительно нужно. Поскольку у вас есть тесты, написанные на нижних уровнях, которые могут вставлять макеты через аргументы, вам, вероятно, больше не нужно макетировать много источников данных на верхнем уровне.

Если после всего этого DI все еще громоздок, подумайте о внедрении одного объекта, который содержит ссылки на все ваши источники данных. Всегда легче вводить одну вещь, а не много.

7 голосов
/ 04 марта 2009

Использовать контейнер 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 моя жизнь в тестировании значительно упростилась.

5 голосов
/ 08 марта 2009

У меня нет вашего кода, но моя первая реакция заключается в том, что ваш тест пытается сказать вам, что у вашего объекта слишком много соавторов. В подобных случаях я всегда нахожу, что там отсутствует недостающая конструкция, которая должна быть упакована в структуру более высокого уровня. Использование контейнера с автоблокировкой просто заглушает обратную связь, которую вы получаете от своих тестов. См. http://www.mockobjects.com/2007/04/test-smell-bloated-constructor.html для более подробного обсуждения.

5 голосов
/ 03 марта 2009

Вы правы, что это может быть громоздко.

Сторонник методологии насмешек указал бы, что код написан неправильно, чтобы быть с. То есть вы не должны создавать зависимые объекты внутри этого метода. Скорее, API инъекции должны иметь функции, которые создают соответствующие объекты.

Что касается макетирования 6 различных объектов, это правда. Однако, если вы также тестировали эти системы, эти объекты уже должны иметь инфраструктуру моделирования, которую вы можете использовать.

Наконец, используйте насмешливый фреймворк, который сделает часть работы за вас.

4 голосов
/ 30 апреля 2009

В этом контексте я обычно нахожу утверждения в духе «это указывает на то, что у вашего объекта слишком много зависимостей» или «у вашего объекта слишком много соавторов», чтобы быть довольно ложным заявлением. Конечно, контроллер MVC или форма будут вызывать множество различных сервисов и объектов для выполнения своих обязанностей; в конце концов, он находится на верхнем уровне приложения. Вы можете объединить некоторые из этих зависимостей в объекты более высокого уровня (скажем, ShippingMethodRepository и TransitTimeCalculator объединяются в ShippingRateFinder), но это только так, особенно для этих объектов верхнего уровня, ориентированных на представление. Это на один объект меньше, но вы только запутали фактические зависимости одним слоем косвенного обращения, а не удалили их.

Один кощунственный совет - сказать, что если вы зависимы, внедряете объект и создаете интерфейс для него, который вряд ли когда-либо изменится (вы действительно собираетесь добавить новый MessageBoxService при изменении кода? Действительно? ), то не беспокойтесь. Эта зависимость является частью ожидаемого поведения объекта, и вы должны просто протестировать их вместе, поскольку в интеграционном тесте заключается реальная ценность для бизнеса.

Другой кощунственный совет - я обычно вижу небольшую полезность в модульном тестировании контроллеров MVC или Windows Forms. Каждый раз, когда я вижу, как кто-то насмехается над HttpContext и проверяет, был ли установлен cookie, я хочу кричать. Кому интересно, если AccountController установит куки? Я не. Файл cookie не имеет ничего общего с обработкой контроллера как черного ящика; Интеграционный тест - это то, что необходимо для тестирования его функциональности (хм, вызов функции PrivilegedArea () завершился неудачно после Login () в интеграционном тесте). Таким образом, вы избежите аннулирования миллиона бесполезных модульных тестов, если формат файла cookie для входа в систему изменится.

Сохраните модульные тесты для объектной модели, сохраните интеграционные тесты для уровня представления и по возможности избегайте ложных объектов. Если издеваться над конкретной зависимостью сложно, пришло время проявить прагматичность: просто не выполняйте модульный тест, вместо этого напишите интеграционный тест и перестаньте тратить свое время.

3 голосов
/ 30 апреля 2009

Простой ответ заключается в том, что код, который вы пытаетесь проверить , делает слишком много . Я думаю, что соблюдение принципа одиночной ответственности может помочь.

Метод кнопки Save должен содержать только вызовы верхнего уровня для делегирования вещей другим объектам . Эти объекты затем могут быть абстрагированы через интерфейсы. Затем, когда вы тестируете метод кнопки «Сохранить», вы проверяете взаимодействие только с макетированными объектами .

Следующим шагом является написание тестов для этих классов более низкого уровня, но все должно стать проще, поскольку вы тестируете их только изолированно. Если вам нужен сложный код настройки теста, это хороший индикатор плохого дизайна (или плохого подхода к тестированию).

Рекомендуемое значение:

  1. Чистый код: руководство по гибкому программному обеспечению
  2. Руководство Google по написанию тестируемого кода
1 голос
/ 03 марта 2009

Конструктор DI - не единственный способ сделать DI. Поскольку вы используете C #, если ваш конструктор не выполняет значительной работы, вы можете использовать Property DI. Это значительно упрощает работу конструкторов вашего объекта за счет сложности вашей функции. Ваша функция должна проверить недействительность любых зависимых свойств и выдать InvalidOperation, если они равны нулю, прежде чем она начнет работать.

0 голосов
/ 03 марта 2009

Когда сложно что-то тестировать, обычно это признак качества кода, когда код не тестируется (упомянуто в этом подкасте , IIRC). Рекомендуется реорганизовать код, чтобы его можно было легко протестировать. Некоторые эвристические решения для решения, как разделить код на классы: SRP и OCP . Для получения более конкретных инструкций необходимо просмотреть соответствующий код.

...