2009-03-03 3 views
9

C#, nUnit и Rhino Mocks, если это окажется применимым.TDD и DI: инъекции зависимостей становятся громоздкими

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

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

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

+0

Это помогло бы предположить улучшения, если бы вы показать код. –

ответ

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.

+0

Этот подход только скрывает сложность, он не освобождает вас от этого.Коренная проблема здесь в тестируемом коде, а не в самом тестовом коде. –

+0

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

5

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

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

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

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

1

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

+0

не согласны, что делает его основанным на свойствах не упрощает, просто скрывает сложность. – eglasius

+0

Ну, упрощая, я имею в виду, что он позволяет переносить сложность из одного места в другое. Это может фактически упростить ситуацию с точки зрения тестирования или других аспектов системы, позволяя вам обрабатывать более простые части в небольших кусках. – Randolpho

0

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

15

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

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

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

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

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

+0

@ дядя Боб. вы упомянули точно, что я делаю, в намеках. – vijaysylvester

+1

Пожалуйста, нет объектов Бога. Я потратил слишком много времени на очистку объектов «все-зависимые», которые разрушают модульность, заставляя весь код зависеть от всех зависимостей. –

+0

@Uncle Bob, Спасибо. Первые 2 предложения ударили меня. –

5

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

4

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

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

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

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

3

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

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

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

Рекомендуемая литература:

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