Единичные тесты по определению !, о поведении внутри a юнит (как правило, один класс): чтобы выполнить их правильно, вы стараетесь изо всех сил изолировать тестируемое устройство от его взаимодействия с другими устройствами (например, с помощью насмешек, внедрения зависимостей и т. д.).
OCP относится к поведению в единицах («программных объектах»): если объект A использует объект B, он может расширить его, но не может его изменить. (Я думаю, что акцент в статье в Википедии исключительно на изменениях исходного кода неуместен: проблема относится ко всем изменениям, независимо от того, получены ли они с помощью изменений исходного кода или другими средствами времени выполнения).
Если A изменил B в процессе его использования, то на несвязанную сущность C, которая также использует B, впоследствии может быть оказано неблагоприятное воздействие. В этом случае правильные юнит-тесты обычно НЕ улавливают поломку, потому что она не ограничивается юнитом: это зависит от тонкой, специфической последовательности взаимодействий между юнитами, при этом A использует B и , тогда C также пытается используйте B. Интеграционные, регрессионные или приемочные тесты МОГУТ его поймать, но вы никогда не можете положиться на такие тесты, обеспечивающие идеальное покрытие возможных путей кода (это достаточно сложно даже в модульных тестах, чтобы обеспечить идеальное покрытие внутри одного объекта / объекта! 1017 *
Я думаю, что в некотором смысле наиболее яркой иллюстрацией этого является противоречивая практика исправления обезьян , разрешенная в динамических языках и популярная в некоторых сообществах практиков таких языков (не всех! -). Исправление обезьян (MP) - это изменение поведения объекта во время выполнения без изменения его исходного кода, поэтому оно показывает, почему я думаю, что вы не можете объяснить OCP исключительно в терминах изменений исходного кода.
MP хорошо показывает пример, который я только что привел. Модульные тесты для A и C могут каждый проходить с летающими цветами (даже если они оба используют реальный класс B вместо того, чтобы издеваться над ним), потому что каждый блок, по сути, работает нормально; даже если вы тестируете ОБА (так что это уже далеко от тестирования UNIT), но бывает так, что вы тестируете С до А, все выглядит нормально. Но, скажем, A обезьяна исправляет B, устанавливая метод B.foo, чтобы он возвращал 23 (как нужно A) вместо 45 (как документально поставляет B, а C полагается). Теперь это нарушает OCP: B должен быть закрыт для модификации, но A не соблюдает это условие и язык его не применяет. Затем, если A использует (и модифицирует) B, а затем очередь C, C работает в состоянии, в котором он никогда не тестировался - то, где B.foo, недокументировано и удивительно, возвращает 23 (тогда как всегда вернул 45 за все время тестирования ...! -).
Единственная проблема с использованием MP в качестве канонического примера нарушения OCP заключается в том, что оно может породить ложное чувство безопасности среди пользователей языков, которые явно не допускают MP; на самом деле, через файлы конфигурации и опции, базы данных (где каждая реализация SQL допускает ALTER TABLE
и т. п .;-), удаленное взаимодействие и т. д. и т. д., каждый достаточно большой и сложный проект должен следить за нарушениями OCP, даже если бы это было написано на Eiffel или Haskell (и тем более, если якобы «статический» язык фактически позволяет программистам вставлять в память все, что они хотят, до тех пор, пока у них есть правильные заклинания приведения, как это делают C и C ++ - теперь ЭТО такую вещь, которую вы определенно хотите отловить в обзорах кода; -).
«Закрыт для модификации» - это цель разработки - это не значит, что вы не можете изменить исходный код объекта, чтобы исправить ошибки, если такие ошибки найдены (и тогда вам понадобится обзоры кода, дополнительные тесты, включая регрессионные тесты для исправления ошибок и т. д., конечно).
Единственной нишей, в которой я видел «немодифицируемое после выпуска», широко применяемое, являются интерфейсы для моделей компонентов, таких как старый добрый COM от Microsoft - ни один опубликованный COM-интерфейс никогда не может изменяться (так что в итоге вы получите IWhateverEx
IWhatever2
, IWhateverEx2
и т. П., Когда исправления интерфейса оказываются необходимыми - никогда не меняется на исходный IWhatever
! -).
Даже в этом случае гарантированная неизменность применяется только к интерфейсам - реализациям этих интерфейсов всегда разрешено исправлять ошибки, настраивать оптимизацию производительности и т. П. (" сделайте все правильно в первый раз «просто не работает при разработке ПО: если вы можете выпускать программное обеспечение только тогда, когда на 100% уверены, что оно имеет 0 ошибок и максимально возможную и необходимую производительность на каждой платформе, на которой оно когда-либо будет использоваться, вы» никогда не выпускал ничего, соревнование съело бы твой обед, и ты обанкротился бы ;-). Опять же, для исправления и оптимизации таких ошибок, как обычно, нужны обзоры кода, тесты и т. Д.
Я полагаю, что спор в вашей команде вызван не исправлениями ошибок (кто-то спорит за запрет этих ? -) или даже оптимизацией производительности, а скорее вопросом о том, где разместить новые функции - Должны ли мы добавить новый метод foo
в существующий класс A
или, скорее, расширить A
в B
и добавить foo
только в B
, чтобы A
оставался «закрытым для модификации»? Модульные тесты сами по себе еще не отвечают на этот вопрос, поскольку они могут не использовать все существующие варианты использования A
(A
может быть отключено, чтобы изолировать другую сущность, когда эта сущность будет проверена ...), поэтому вам нужно пойти на один уровень глубже и посмотреть, что foo
точно означает , или может быть , делая.
Если foo
является просто средством доступа и никогда не изменяет экземпляр A
, для которого он вызывается, то добавление его явно безопасно; если foo
может изменить состояние экземпляра и последующее поведение, наблюдаемое другими существующими методами, , тогда у вас есть проблема. Если вы уважаете OCP и поместите foo
в отдельный подкласс, ваши изменения будут очень безопасными и рутинными; если вам нужна простота ввода foo
прямо в A
, , тогда вам делать нужны подробные обзоры кода, легкие тесты "парной интеграции компонентов", проверяющие все варианты использования A
, и так далее. Это не ограничивает ваше архитектурное решение, но оно четко указывает на различные затраты, связанные с любым выбором, поэтому вы можете планировать, оценивать и расставлять приоритеты соответствующим образом.
Дикта и принципы Мейера не являются Священной Книгой, но с должным критическим отношением их очень стоит изучить и обдумать в свете ваших конкретных, конкретных обстоятельств, поэтому я благодарю вас за это в этом случае. ! -)