Как стать автором
Обновить

Комментарии 83

Помните один из минусов слабо связанных архитектур? Из-за того, что в них преобладает позднее связывание

Не стоит, все-таки, путать слабое связывание и позднее связывание. Можно иметь слабо связанную архитектуру, построенную полностью на раннем связывании.

Вызов метода DoHeavyBreath у класса Luke — логическая ошибка не видимая компилятору.

Что? Если предположить, что это C#, то компилятор радостно бросит тут ошибку.

(а вот ваш второй пример, с двумя оверрайдами — это как раз логическая ошибка проектирования)

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

А что нам мешало сделать это раньше?

Мы создаем метод для тестирования метода main. Простейшим способом

Простейший способ — это не ловить ошибку вообще. Зачем вам over-specification?

В общем, ничего не имею против TDD, но вот ваша статья как-то совершенно ни о чем.
пример можно бесконечно улучшать, чего я не делал, а просто показал плюс один довод в пользу TDD. Если раньше я рассматривал его как полезный инструмент, а теперь вижу как необходимый и попытался обьяснить почему.
Иногда когда рождаются какие-то идеи, то у меня привычка гуглить, не додумался ли кто-то до такого же, а не кричать, что я первый что-то придумал. Просто интересно не возникало ли еще у кого-то подобных ассоциаций. Интересно послушать.
Я не вижу в вашей статье ни одного реального аргумента в пользу TDD. Во-первых, потому, что ваш пример ловится компилятором, а во-вторых, потому, что даже для ошибок позднего связывания достаточно интеграционных тестов (напоминаю, что TDD и покрытие кода тестами — не одно и то же).
Можете рассматривать это не как довод, а как другой взгляд. Это просто мои ассоциации, которые я посчитал могут быть интересными, и захотел услышать того, кто думает так же.
И можно поподробнее, что именно ловится компилятором?
А, извините, я был невнимателен. Не ловится (вызов метода DoHeavyBreath у класса Luke). Но его и не должно быть, потому что класс Luke не является наследником класса Dart, чтобы вы ни думали про SW.

Так что это типичная ошибка проектирования (если конкретнее, нарушение LSP), и TDD тут не при чем.
Я вылечился немного от перфекционизма, поэтому получился такой пример :)
Если хотите, могу придумать для Вас более адекватный. Просто подумал, что такого достаточно, чтоб уловить суть идеи.
Ваша проблема в том, что вы не видите разницы между TDD и просто покрытием кода тестами. Пока что приведенный вами пример — это просто про то, что код нужно тестировать.

Ну да, нужно, спасибо, кэп.
Ох уж эти приверженцы юнит-тестов…
ну конечно, после смены темы — я бы тоже заминусовал
Почему?
Потому что теперь это не сарказм…
Заминусовали, кстати, до смены.
«Писать тесты» != «Практиковать TDD»
Про само TDD в статье ни слова.
Наверное мне было очевидно, что если я пишу про покрытие тестами и упоминаю в заголовке TDD, то я и имею его в виду. Написать тесты конечно не достаточно, надо еще и запускать их. Вообще если придираться, то я бы сказал, что статья про continuous testing, но все это наиболее органично смотрится в сочетании с TDD. Не хотелось расписывать все подробно (может еще и определение TDD привести?).
Из всех коментариев на данный момент, я еще не услышал комментария по сути, а только цепляния за какие-то мелочи. Ну не нравится или непонятна статья — спросите или проходим дальше по салону.
Я про тесты узнал как раз из-за того, что часто встречал упоминание ТДД и, конечно, и сам с этого начал. Сейчас я не представляю, как можно обходиться без тестов, но ТДД я не использую и ваше очевидное «тесты => TDD» таковым не считаю.

Это и есть «по сути». Вы собрались об одном рассказать (о ТДД, в заголовке), а рассказали об одном из плюсов написания тестов. Причем, если на тему ТДД есть еще разные мнения, то на счет тестов, вроде, все понятно и однозначно: тесты — это хорошо.

В итоге, получилось капитанство — вроде и разумные вещи сказали, а в чем цель была — не понятно.
Не стоит всех записывать в дураки :) Просто TDD — это идиология, подход к разработке, а не сами тесты.

А то, что описали Вы — всё равно что описывать преимущества ООП, на основе того, что в IDE функции/поля легче набираются
Как-то всё очень сумбурно, а главное не видно преимуществ именно TDD, а не просто покрытиями тестами.

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

Не совсем понял отчего ожидал? От каких-то инструментов?
Угу, набросал тесты и генерируется код, который эти тесты проходит.
Угу.

switch (testNumber)
{
 ...
}


Узнать тесты — мечта олимпиадника :)
Если я не ошибаюсь, примеры говорят о пользе юнит тестов.

А про TDD (test driven development — разработка через тестирование, в котором СНАЧАЛА пишутся тесты, а после, по ним разрабатывается код) здесь реально ничего нет
чтобы я писал рабочий код

в этом Вам и помогут юнит-тесты, потому что на каком-то этапе сложности архитектуры начнете спотыкаться
Юнит-тесты (сами по себе) никак (вообще никак) не влияют на архитектуру (в сторону ее улучшения). Более того, периодически юнит-тесты архитектуру только усложняют (совершенно без необходимости).
Снижение связанности — это уже плохо?
Это никак (при отсутствии требований на конкретные параметры приложения). Не говоря уже о том, что сами по себе юнит-тесты его не то что бы гарантируют.
Ок, что ты тогда считаешь хорошей архитектурой?
Ту, которая оптимальным образом решает поставленную бизнес-задачу с учетом существующих ограничений.

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

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

Мы привыкли не думая вносить модульность, расширяемость, заменяемость и прочие блабла в список обязательных атрибутов решения; но бизнесу это не надо. И за юнит-тесты бизнес тоже не платит (как бы мы их не любили).

Ну и да, типичный пример навязанных юнит-тестами ограничений — это, конечно, разрыв зависимостей со временем (вообще никак не нужный для хорошей архитектуры).
Перефразирую твой комментарий:
1. «Не забивайте гвозди микроскопами». Спасибо, кэп.
2. «Софт должен зарабатывать деньги для бизнеса, а не ЧСВ для разработчиков». Еще раз спасибо, кэп.

Только, имхо, к теме это имеет отдаленное отношение. Только не надо демагогии про то, что архитектура одноразового скрипта — это тоже архитектура ;)

Уточню scope — я выше говорил о типичных «больших энтерпрайзненьких проектах».
С какого перепуга разрыв зависимостей и снижение cohesion — это плохо?
Так я не говорю, что это плохо, я говорю, что само по себе это никак. Ну вот реально никак. Потому что ты в плюсах получаешь низкую связность, а в минусах — отсутствие (некоторых) статических проверок и ухудшение читаемости кода. Баш на баш.

Юнит-тесты (по крайней мере, в .net, с его технологической базой) навязывают существенно больший уровень разрыва зависимостей, чем реально нужно для архитектурной целостности системы (а это, заметим, плохо, как любой экстремизм).
Проблема со статическими проверками обходится контрактами, а ухудшение читаемости кода… ну, не знаю. Я пока не видел примеров, где читаемость ухудшалась от снижения связанности.
Я вот регулярно вижу. Просто мы привыкли уже это игнорировать, но в реальности там лишний уровень абстракции.
Как именно лишний слой абстракции снижает читаемость?
Представь себе типичную ситуацию: у тебя есть зависимость, абстрагированная за интерфейс. Теперь, находясь внутри некоего пользователя этой зависимости ты понимаешь, что тебе надо проследить за кодом, который внутри нее, потому что ты не понимаешь, как это работает. Вместо того, чтобы попасть сразу в имплементацию, ты попадаешь в интерфейс (традиционно не документированный), а из него тебе надо перейти в реализацию, и хорошо, если она одна.

Понятно, что этого не бывает в идеальном коде, но идеальный код и читать не надо.
Стоп-стоп.

Так что снижает читаемость — разрыв зависимости или кривой интерфейс и кривая декомпозиция ответственностей? Кто виноват в том, что мне понадобилось лезть за интерфейс «в черный ящик»?
И то, и другое. Кривой интерфейс ведет к тому, что приходится лезть в ящик, абстракция приводит к тому, что это сложнее.

Будь реалистом, интерфейс всегда течет.
Интерфейс течет независимо от того как именно мы его дергаем. ИМХО, но слабая связь по моему субъективному впечатлению течет меньше. Точнее у разработчика меньше возможностей устроить протечку или воспользоваться существующей. (я тут уже несколько дней не могу отойти от увиденного HttpContext'а в DAL).

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

Вопрос инструментария короче.
Alt-End, проще говоря. Работает (удобно) только если одна имплементация. И не работает, если у тебя под курсором просто метод. Соответственно, поведение не униформно, в привычку не вбивается.

А так я в курсе.
По-моему, это зависит от самого слоя. Насколько понятна его роль, например. Если у меня был такой код:

IRepository rep = Factory.Get<IRepository>();

А потом Архитектор ввел какой то новый слой, логики которого я не понимаю до конца, то для меня снизится читабельность когда я увижу что-то вроде:

IRepository rep = Preprocessor.DoSomeStrangeLogic(Factory.Get<IRepository>());
Стукните архитектора по голове и спросите почему он ответственность по подготовке репозитария к работе вынес за пределы фабрики снизив cohesion.
BTW, вот как раз cohesion не надо бездумно снижать, потому что методы в интерфейсе должны иметь высокий семантический cohesion (в том смысле, что методы одного интерфейса должны иметь связанные обязанности, а иначе нахрена они в одном интерфейсе).
Бррр… Конечно, не cohesion, а coupling, сорри.

Я от разрыва зависимостей вижу только пользу — чем меньше кусочки и чем меньше между ними связей (до определенных пределов, конечно) — тем проще управлять системой и заменять/обновлять ее части (старое доброе «разделяй и властвуй»).
А это в итоге имеет прямую ценность для бизнеса в виде снижения издержек на изменения.
готов поспорить, что этот одноразовый скрипт вы таки тестируете на тестовой базе данных, а не запускаете сразу фигачить начистовую. И какая разница, создавать тестовую БД или написать пару тестов, которые будут проверять правильность работы этого скрипта?
готов поспорить, что этот одноразовый скрипт вы таки тестируете на тестовой базе данных

Поспорьте. Лучше на что-нибудь крупное, чтобы мне было приятнее.

И какая разница, создавать тестовую БД или написать пару тестов, которые будут проверять правильность работы этого скрипта?

Во-первых, тестовая БД все равно есть. А во-вторых, «пара тестов» требуют изменения архитектуры. Вы когда-нибудь пробовали покрывать тестами powershell? А T-SQL?
Сами по себе, да, не влияют. Но если стремиться делать тесты как можно проще, то очень даже влияют. Формально после этого архитектура может и усложнится (больше уровней абстракций появится, больше параметров в функции/методы нужно будет передавать, а не глобальные переменные использовать :) ), но вот вносить в неё изменения будет проще, даже если забыть про функцию тестов «контроль целостности».
ИМХО, больше уровней абстракции не обязательно равно усложнению архитектуры. Нужно отталкиваться от параметров «цикломатическая сложность», «связность» и «связанность», а не от количества уровней и классов.
«связность» и «связанность»
В смысле связность и сцепление? Немного путаюсь в русских определениях. Или еще лучше coupling, cohesion?
Да, можно cohesion и coupling ;)
Но если стремиться делать тесты как можно проще, то очень даже влияют.

У нас с вами явно разное понимание «тестов как можно проще». Опыт показывает, что в более-менее объемном приложении со сложным графом зависимостей сетап теста — штука существенно более сложная, чем сам код.

больше параметров в функции/методы нужно будет передавать

Больше параметров — сложнее сетап — сложнее (а не проще) тест.

но вот вносить в неё изменения будет проще

Это вы исходите из того, что в архитектуру надо будет вносить изменения. А для меня это не догма.
У нас вообще по многим вопросам разное понимание :) Возможно обусловленное масштабами проектов над которыми мы работаем. В частности, сложность тестов я не меряю большим количеством параметров, сложностью их подготовки. Одно дело вызвать в сетапе пускай десяток, но обычных new со скалярными параметрами конструктора, совсем другое подготовить пускай всего один, но сложный стаб, получающий и возвращающий объекты с трехэтажной агрегацией, да ещё и считывающий и меняющий внутренне состояние фэйкового объекта.
Соглашусь с предыдущим ораторами — в тексте ничего про TDD нет.
Данная статья показывает преимущества просто юнит-тестирования. Показывает правда плохо, вы написали только об самом простом аспекте тестирования — отлов необработанных исключений, а основные аспекты темы не раскрыты, а именно:
1. прогнозирование поведения объекта тестирования, когда не все слои приложения сделаны. Допустим вы написали бизнес-логику и слой доступа к базам данных, а веб-приложения еще не написано, или когда работаете в команде и вам надо проверить вашу часть кода.
2. написание теста для класса хорошо показывает насколько независимым вы сделали ваш объект ( DI и IoC)
3. тесты очень хорошо помогают, если вам приходится разбираться в чужом коде (да и в своем кстати тоже). Если мне достается чей-то проект (для редактирования или кодревью) и там есть написанные юнит-тесты — это как бальзам на душу.
и тд…
По поводу TDD это отдельная тема, чесно говоря, если про юнит-тестирование я могу сказать, что это всегда хорошо, то TDD от случая к случаю — все зависит от поставленных задач и от общего стиля работы команды разарботчиков.
точно, забыл проверить
Спасибо за интересную мысль. Мысль рассматривать тесты как навесные проверки для своего кода мне почему-то в голову не приходила. Возможно, потому что тесты — штука очень конкретная. Если дополнить непрерывный прогон тестов генерацией этих самых тестов по определенным правилам, будет интереснее. Поясню свою мысль — есть у нас интерфейс. Написали тесты, которые проверяют его семантику. Я хочу, чтобы эти тесты выполнялись для каждой реализаии интерфейса, появляющейся в проекте. Без лишних телодвижений со стороны разработчикОВ.
Я хочу, чтобы эти тесты выполнялись для каждой реализаии интерфейса, появляющейся в проекте

А как же сетап, который для каждой реализации может быть свой?
И это только одна из проблем, согласен. Не судите строго, идея пришла мне в голову после прочтения статьи, надо ещё обдумать. Есть вроде инструмент, которые автоматически генерируют код, создающий объект заданного типа.
Есть вроде инструмент, которые автоматически генерируют код, создающий объект заданного типа.

Имя им легион (причем даже уже код генерировать не надо). Проблема, однако же, в том, что мало создать объект, надо его еще сконфигурировать (передать зависимости, сконфигурированные на нужное поведение). И эта конфигурация для каждого объекта мало того, что своя, так еще и может отличаться для разных тестов.
Можно название двух-трех таких инструментов?
Создания объекта заданного типа?

Activator.CreateInstance() встроен в .net framework с первой версии.
Нет, я просто неправильно понял. Я подумал об инструментах, генерирующих код по юнит тестам.
Вы имеете в виду работающий код? Это, знаете ли, плохо решаемая задача, потому что это то же самое, что автоматически писать программы по требованиям.

А просто декларации, соответствующие использованию, генерятся в VS начиная с прошлой, что ли, версии (Generate Method Stub).
Ну мне лично кажется, что это осуществимо при достаточной формальности и полноты описания бизнес логики. Может не вся программа на 100% будет генерироваться, но какие то блоки могут быть автоматически сгенерированы. Это только мысли и догадки.
В этом случае просто ваше «формальное и полное описание бизнес-логики» и будет тем кодом, который надо будет тестировать. Проблема никуда не исчезнет, а просто будет перенесена в другой уровень (который, заметим, ничем не лучше «просто кода»).

Для таких вещей делают DSL (код на котором, естественно, тоже тестируют).
Да, по сути программировать надо будет этой «бизнес логикой». Плюс я вижу в основном в том, что это будет более человеческий язык. Легче в изучении и понимании.
После обсуждения с коллегами идея формулируется так:
Автоматическая привязка тестов к коду. Для теста задается условие (шаблон или как там в АОП, поинткат?), при выполнении которого устанавливаются значения некоторых переменных и происходит запуск теста (переменные могут в тесте использоваться). Если при этом не хватает какого-то кода привязки, который не может быть сгенерирован — тест не проходит.

Пример 1: шаблон такой: для каждого метода с именем $© get${F}() в классе ${T}, если существует метод set${F}( ${C} ), выполняем такой тест:
${T} t = create${T}();
${C} f = create${C}();
t.set${F}( f );
assertTrue( t.get${F}() == f );

Если методы create для T и С не найдены — тест не проходит.

Пример 2: для каждого интерфейса ${I} и класса ${C}, который его реализует, надо выполнить заданный тест:
void test( ${I} i ) {

}
с параметром create${C}

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

наверное, возможностей create хватит не всегда, надо еще какой-то механизм придумать
Пример 1

А зачем это тестировать? Это тривиальный код (не говоря уже о том, кто будет писать методы create, особенно учитывая, что они могут быть для разных тестовых случаев)

Пример 2

И снова — кто будет писать конкретные тестовые фабрики? Вся сложность-то как раз в них, недаром Мезарос столько внимания уделяет как раз разным вариантм fixture.

А так, что, что вы описываете — типичный параметризованный тест, все давно съедено и придумано. У меня так контроллеры проверяются на то, что все зависимости доступны.
Цель примера — продемонстрировать идею. Как правило, он получается слишком простым. Но в более сложном примере сложнее разобраться.

При появлении нового контроллера ваши тесты перестают проходить?
Цель примера — продемонстрировать идею. Как правило, он получается слишком простым. Но в более сложном примере сложнее разобраться.

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

При появлении нового контроллера ваши тесты перестают проходить?

Да.
Да, наверное, fixture — это обобщение моего create

C помощью какого мехнизма тесты перестают проходить при появлении нового контроллера?
C помощью какого мехнизма тесты перестают проходить при появлении нового контроллера?

Параметризованный тест + autodiscovery исходных данных. Если бы MsTest умел — был бы вообще data-driven с кастомным поставщиком, но, к сожалению, это не так просто, как хотелось бы.
Видимо, про autodiscovery я и говорю. Это где-то реализовано уже? Java-мир мне ближе :)
Реализовано что? Поиск всех классов в сборке, удовлетворяющих заданным критериям? Это тривиальная задача для любой платформы с reflection.
Не только классов. Шаблоны могут быть сложнее. Я не спорю, написать можно. Интересно, чтобы из коробки работало.
А какая разница, что искать?

«Из коробки» такие вещи обычно работают в AOP-фреймворках и DI-контейнерах. А в тестах это слишком редко нужно.
Спасибо за первый комментарий по сути. Да, мысль о генерации кода по тестам мне еще не приходила, довольно интересно. И думаю, что реализация не за горами.
Честно говоря, обидно, когда топик минусуют те, кто его не понял. В результате его не увидят те, кто может оценить.
Это не мои проблемы, что система тут так устроена, поэтому предпочитаю не обращать внимания. Покидаю ссылки друзьям/знакомым, может они какие идеи еще подкинут.
А что мешает объявить функции абстрактными, в данном конкретном примере?
Вечерком допишу нормальный пример, а то я смотрю без него слишком много вопросов и минусов.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории