Когда использовать интерфейсы (модульное тестирование, IoC?)

17

Я подозреваю, что допустил ошибку школьника и ищу уточнения. Многие классы в моем решении (C #) - осмелюсь сказать, что большинство - я закончил тем, что написал соответствующий интерфейс для. Например, интерфейс «ICalculator» и класс «Calculator», который его реализует, хотя я вряд ли заменю этот калькулятор другой реализацией. Кроме того, большинство этих классов находятся в том же проекте, что и их зависимости - они действительно должны быть internal, но в конечном итоге стали publicпобочным эффектом реализации их соответствующих интерфейсов.

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

1) Первоначально я думал, что интерфейс необходим для создания макетов модульного теста (я использую Moq), но с тех пор я обнаружил, что класс может быть смоделирован, если его члены virtual, и у него есть конструктор без параметров (поправьте меня, если Я не прав).

2) Первоначально я думал, что интерфейс необходим для регистрации класса в IoC Framework (Castle Windsor), например

Container.Register(Component.For<ICalculator>().ImplementedBy<Calculator>()...

когда на самом деле я мог просто зарегистрировать конкретный тип против себя:

Container.Register(Component.For<Calculator>().ImplementedBy<Calculator>()...

3) Использование интерфейсов, например параметров конструктора для внедрения зависимостей, приводит к «слабой связи».

Так я сошел с ума от интерфейсов ?! Мне известны сценарии, в которых вы «обычно» используете интерфейс, например, публикуете публичный API или что-то вроде «подключаемой» функциональности. Мое решение имеет небольшое количество классов, которые подходят для таких вариантов использования, но мне интересно, если все другие интерфейсы не нужны, и должны быть удалены? Что касается пункта 3) выше, не буду ли я нарушать "слабую связь", если я буду делать это?

Изменить : - Я просто играю с Moq, и мне кажется, что методы должны быть открытыми и виртуальными, и иметь открытый конструктор без параметров, чтобы иметь возможность их издеваться. Похоже, у меня не может быть внутренних классов?

Эндрю Стивенс
источник
Я почти уверен, что C # не требует, чтобы вы делали класс общедоступным для реализации интерфейса, даже если интерфейс общедоступен ...
vaughandroid
1
Вы не должны проверять частных пользователей . Это можно рассматривать как индикатор анти-паттерна класса бога. Если вы чувствуете необходимость модульного тестирования приватной функциональности, то обычно хорошей идеей будет инкапсулировать эту функциональность в свой собственный класс.
Спойл
@ Говорите, что рассматриваемый проект - это проект пользовательского интерфейса, где казалось естественным сделать все классы внутренними, но они все еще нуждаются в модульном тестировании? Я читал в другом месте о том, что нет необходимости тестировать внутренние методы, но я никогда не понимал, почему.
Эндрю Стивенс
Сначала спросите себя, что такое тестируемое устройство. Это целый класс или метод? Это устройство должно быть общедоступным, чтобы его можно было правильно протестировать . В проектах пользовательского интерфейса я обычно разделяю тестируемую логику на контроллеры / модели представления / сервисы, которые имеют открытые методы. Фактические виды / виджеты подключаются к контроллерам / моделям / сервисам представления и тестируются с помощью регулярного тестирования дыма (т.е. запускают приложение и начинают щелкать). У вас могут быть сценарии, которые должны проверять базовую функциональность, а наличие модульных тестов на виджетах приводит к хрупким тестам и должно выполняться с осторожностью.
Спойл
@ Говорите, так вы публикуете свои методы виртуальной машины только для того, чтобы сделать их доступными для наших модульных тестов? Возможно, именно здесь я иду не так, стараясь изо всех сил сделать все внутренним. Я думаю, что мои руки могут быть связаны с Moq, хотя, кажется, что классы (а не только методы) должны быть публичными, чтобы высмеивать их (см. Мой комментарий к ответу Теластина).
Эндрю Стивенс

Ответы:

7

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

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

Telastyn
источник
1
Спасибо за ответ. Как уже упоминалось, есть несколько ошибочных причин, почему я сделал то, что сделал, хотя я знал, что большинство интерфейсов никогда не будет иметь более одной реализации. Частично проблема заключается в том, что я использую инфраструктуру Moq, которая отлично подходит для макетных интерфейсов, но для макетирования класса он должен быть общедоступным, иметь общедоступный ctr без параметров, а методы, которые нужно смоделировать, должны быть «общедоступными виртуальными»).
Эндрю Стивенс
Большинство классов, которые я тестирую, являются внутренними, и я не хочу делать их общедоступными, чтобы сделать их тестируемыми (хотя вы можете поспорить, для этого я и написал все эти интерфейсы). Если я избавлюсь от своих интерфейсов, то, вероятно, мне нужно будет найти новый фреймворк!
Эндрю Стивенс
1
@AndrewStephens - не все в мире нуждается в насмешках. Если классы достаточно надежны (или тесно связаны), чтобы не нуждаться в интерфейсах, они достаточно надежны, чтобы использовать их как есть в модульном тестировании. Время / сложность их использования не стоит преимуществ тестирования.
Теластин
-1, так как часто вы не знаете, сколько разработчиков вам нужно, когда вы впервые пишете код. Если вы используете интерфейс с самого начала, вам не нужно менять клиентов при написании дополнительных разработчиков.
Уилберт
1
@wilbert - конечно, маленький интерфейс более читабелен, чем класс, но вы не выбираете ни тот, ни другой, у вас есть либо класс, либо класс и интерфейс. И вы, возможно, слишком много читаете в мой ответ. Я не говорю, что никогда не создавайте интерфейсы для одной реализации - на самом деле, большинство должно, так как для большинства взаимодействий требуется такая гибкость. Но прочитайте вопрос еще раз. Создание интерфейса для всего - просто грузовое культивирование. IoC не является причиной. Насмешки не причина. Требования являются причиной для реализации этой гибкости.
Теластин
4

1) Даже если конкретные классы являются поддельными, все равно требуется, чтобы вы virtualсоздавали члены и предоставляли конструктор без параметров, который может быть навязчивым и нежелательным. Вы (или новые члены команды) скоро обнаружите, что систематически добавляете virtualконструкторы и параметры без параметров в каждый новый класс, не задумываясь, просто потому, что «так все и работает».

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

3) Как это ложь?

Я считаю, что интерфейсы отлично подходят для определения протоколов обмена сообщениями между взаимодействующими объектами. Могут быть случаи, когда необходимость в них является дискуссионной, как, например, для доменных сущностей . Однако, где бы у вас ни был стабильный партнер (т. Е. Внедренная зависимость, а не временная ссылка), с которой вам нужно общаться, вам следует рассмотреть возможность использования интерфейса.

guillaume31
источник
3

Идея IoC состоит в том, чтобы сделать конкретные типы заменяемыми. Таким образом, даже если (прямо сейчас) у вас есть только один калькулятор, который реализует ICalculator, вторая реализация может зависеть только от интерфейса, а не от деталей реализации из Calculator.

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

Если вы делаете ваши уроки общедоступными или внутренними, это на самом деле не связано, и ни один из них не является moq-ing.

Уилберт
источник
2

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

Двойные тесты в основном полезны для изоляции вашего кода от внешнего мира (сторонних библиотек, дискового ввода-вывода, сетевого ввода-вывода и т. Д.) Или от действительно дорогих вычислений. Если вы слишком либеральны с вашей структурой изоляции (вам даже ДЕЙСТВИТЕЛЬНО она нужна? Является ли ваш проект настолько сложным?), То это может легко привести к тестам, связанным с реализацией, и это сделает ваш проект более жестким. Если вы напишете несколько тестов, которые проверяют, что какой-то класс вызвал метод X и Y в этом порядке, а затем передали параметры a, b и c в метод Z, вы ДЕЙСТВИТЕЛЬНО получаете значение? Иногда вы получаете такие тесты при тестировании логирования или взаимодействия со сторонними библиотеками, но это должно быть исключением, а не правилом.

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

Говоря более конкретно об интерфейсах, я считаю, что они полезны в нескольких ключевых случаях:

  • Когда вам нужно отправлять вызовы методов разным типам, например, когда у вас есть несколько реализаций (это очевидно, поскольку определение интерфейса в большинстве языков является просто набором виртуальных методов)

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

  • Когда вам нужно инвертировать управление для защиты границ приложения. Это один из самых мощных способов управления зависимостями. Мы все знаем, что высокоуровневые модули и низкоуровневые модули не должны знать о каждом другом, потому что они будут меняться по разным причинам, и вы НЕ хотите иметь зависимости от HttpContext в вашей доменной модели или от какой-либо инфраструктуры рендеринга PDF в вашей библиотеке умножения матриц. , Заставить ваши граничные классы реализовывать интерфейсы (даже если они никогда не заменяются) помогает ослабить связь между уровнями и резко снижает риск просачивания зависимостей через слои из типов, знающих слишком много других типов.

Сара
источник
1

Я склоняюсь к идее, что, если какая-то логика является частной, то реализация этой логики не должна никого волновать и поэтому не должна проверяться. Если какая-то логика достаточно сложна, и вы хотите ее протестировать, эта логика, вероятно, играет особую роль и должна быть общедоступной. Например, если я извлекаю некоторый код в частном вспомогательном методе, я нахожу, что это обычно то, что можно с таким же успехом размещать в отдельном общедоступном классе (форматере, парсере, преобразователе и т. Д.).
Что касается интерфейсов, я позволил необходимости заглушить или издеваться над классом. Такие вещи, как репо, я обычно хочу иметь возможность заглушки или издеваться. Картограф в интеграционном тесте (обычно) не так уж и много.

Стефан Биллиет
источник