- Да, да, конечно, — сказал им Роман. — Времени у нас мало.
— Мало? — переспросил я. — Ты ошибаешься. Времени у нас нет вообще!
(с) Стругацкие,
Читая топики имеющих отношения к юнит-тестированию
Unit-тесты vs. контрактно ориентированное программированиеАвтор: Lazy Cjow Rhrr
Дата: 12.07.06
,
ООП — перспективыАвтор: NetSlow
Дата: 13.06.06
и конечно
Почему ваш код – отстойАвтор: Владислав Сивяков, Алексей Мудрик (перев
Дата: 20.06.06
у меня возникло стойкое несогласие по некоторым моментам. Но поскольку сообщений было много и мнений было много, то анализ топиков, занял у меня некоторое время. Тем не менее, результат анализа перед вами.
Итак.
Высказывание "Если юнит-тестов нет, то программа не работает" — это догма. И как любая другая догма она является ложной, поскольку не учитывает многообразие реального мира.
Реальный мир для нас, небольшой компании, таков.
Проекты у нас 2-х видов. Долгие, развивающиеся в течение нескольких лет, там много своего кода, меняются они очень вяло. Тестов там тоже много, тем не менее время от времени всплывают сигналы о недостаточном покрытии функционала. Это нормально.
Есть и другие, и о них я буду дальше вести разговор.
Здесь программирование происходит в условиях
перманентной нехватки времени. В таких условиях вопрос: "А где юнит-тесты?" звучит как издевательство. Очевидный ответ на этот вопрос: "А может вам ещё танец живота на столе станцевать?".
Другим аспектом является то, что в таких условиях основная часть времени времени выделяется на битву с окружением. Посмотрите в форум по Java. Подавляющее большинство вопросов: "Какого х###
Hibernate |
Struts |
Jboss |
AnotherCoolButComplexFramework делает не то, что я от него жду и как его добить". Проектирование сильно кренит в сторону "как сделать так, чтобы оно хорошо клеилось к окружению". Божьей помощью оказывается найти в инете работающий пример и разобраться, почему он-таки работает. Если такого примера нет, то приходится подбирать диаметр бубна, цвет ленточек и звук колокольчиков на нём. Процесс этот, как можно догадаться, недетерминированный. (Я не говорю, что библиотеки суть сплошной глюконат, я о том, что чтобы задействовать тот функционал, который находится в библиотеке, нужно следовать определённым правилам, и если нужных сценариев нет в доке и инете, то их (сценарии) придётся вырабатывать самому).
Создаваемое ПО имеет несколько особенностей: кусок "мясца" средних размеров (почему-то называемого заумным термином "Biznizz Lojig"
![](/Forum/Images/smile.gif)
'), и много кода взаимодействующего с окружением, библиотеками и фреймворками. Соотношение меняется от размера к размеру, но как правило "мясца" заметно меньше.
Худо-бедно победив окружение, делается мрачное внутри, но сносное снаружи ПО, с фичами, которые были жирно начёрканы в виде кривых квадратиков со стрелочками на листе формата A4. Эти фичи прорабатываются руками (такое тестирование обязательно) в виде определённого набора вариантов использования. Эти варианты
должны проходить 100% гладко. Однако как только начинаются нестандартные варианты использования, корректная работа увы, не гарантируется. (вероятно только Билли Гейтс может позволить отшутиться на презентации BSoD'а, а для небольших компаний это всё равно что, извините, мордой в дерьмо — неприятно). Вдобавок об отодвигании сроков не может быть и речи.
Короче, такая недружественная практика по отношению к программистам при этом вполне себя оправдывает: продукт в рекордные сроки поставляются клиентам, клиенты кивают головой увидев что нужно, конкуренты курят бамбук. И только программисты слегка оправившись от пережитого "ужоса" понимают, что "ужос" скоро будет опять (очередная порция хотелок), поэтому надо взять рефакторинг и хотя бы причесать код. Опыт показывает, что код, написанный на большой скорости просто ужасен (низкий уровень обобщения, невнятные соглашения, большая избыточность, сильное сцепление), поэтому рефакторить
надо.
Ладно, с условиями разобрались. Теперь перечислим факты о ЮТ, которые играют роль, но о которых не очень охотно говорят.
1. Писать ЮТ — рутина.
Кто не согласен, может попробовать выработать творческий подход к коду наподобие
assertTrue(collection.size()==0);
collection.add("F*ck");
assertTrue(collection.size()==1);
На мой взгляд единственный элемент творчества здесь — это строковая константа.
Эта же причина вызывает острое желание, чтобы тесты более-менее автоматически генерировались, и контракты здесь выглядят как вариант, хотя мне нужны практические данные, чтобы делать какие-то выводы об их применимости. Тесты также можно генерировать из спецификаций, но создавать генератор — долго, да и спецификации витают в воздухе, нужны усилия для формализации.
Если отдать написание тестов в руки молодых специалистов — полученные тесты будут либо источником вечного веселья, либо вечной грусти (как повезёт, иногда бывает даже нормально). Другим опытным программистам разумеется отдавать тоже нельзя — не барское это дело. Вывод: никто кроме самого программиста нормальные тесты не сделает.
Опять же, по этой же причине (я имею ввиду "ЮТ-рутина") Java вызывает иногда ненависть, поскольку там невозможно достаточно просто написать обобщённый класс ClassX, и протестировать только этот обобщённый класс, а использовать Class1..3.
Написание обобщённого кода вызывает ощутимый оверхед, такой, что проще забить на это дело и не парить моск. Для тех, кто сомневается: представьте например, два больших, почти идентичных класса, причём они:
1. наследуют от одного абстрактного, абстрактный класс менять нельзя;
2. немного различаются статическими полями;
3. немного различаются обычными полями;
4. немного различаются внутренними классами (пусть они даже один интерфейс реализуют);
5. немного различаются набором методов и небольшими кусочками в методах, скажем таким образом
String label = getInSomeComplexWay();
String label = super.getLabel();
Как избавиться от дублирования? (Это очень даже реальный пример, два увесистых классика имели всего 43 диффа, дублирование мозолило глаз, но альтернативы? — плодить 43 оператора if, или ещё минимум 3 не самых простеньких класса. Плиз!
![](/Forum/Images/no.gif)
).
Помнится не так давно пролетала ссылка (спасибо eao197) на весёлый текст "Execution in the Kingdom of Nouns", гиперболизм сплошной, но в общем и целом так и есть. (Справедливости ради нужно помянуть Java и хорошими словами, такими как "рефакторинг", "IDE", "reverse engeneering" и т.п.)
2. Написание ЮТ отнимает значительное время и требует внимания.
Этот факт в основном действует как красная тряпка, отвлекая от важных дел. Отвлекать от важных дел он перестанет только когда оно само (написание ЮТ) будет важным делом. А этого не будет никогда, впотому что задача "реализовать 10 архинужных фич к деду Лайну" важнее, чем "реализовать лишь
9 архинужных фич, но чтобы они работали в нестандартных условиях". Тремя словами это будет так: "
worse is better".
Адепты TDD утверждают, что ЮТ неотделимы от непосредственно кода, и поэтому измерить соотношение потраченного времени отдельно на код и на тесты невозможно. Хотя очевидно, что это соотношение существует (на мой взгляд процент должен зависеть от типа проекта; чем проект дольше — тем меньше процент, а абсолютная величина больше), и доля тестов весьма ощутима.
Плюс необходимость переключать внимание с ЮТ на код и обратно — это разбрасывать шары, а потом их снова скрупулёзно собирать... (неплохая аналогия тов. Спольски). Как минимум — некомфортно. Фактически — расход заметного времени именно на переключение (общеизвестная статистика — вхождение "в поток" съедает 15 минут, умножаем это время на количество прерываний=переключений...).
3. Проверять код взаимодействующий с окружением скорее всего не имеет смысла.
(Такая нестрогая формулировка потому, что созданные примеры, показывающие
как надо готовить библиотеки можно назвать интеграционными тестами, они выполняют роль поэтапных проверок при разрешении сложных ситуаций).
Итак, почему скорее не имеет смысла?
Такой код часто имеет следующую особенность: он работает только при грамотной конфигурации, наличия нужных джарок в нужных директориях, правильных переменных окружения и т.п. Примеров куча: деплоймент дескрипторы, plugin.xml, web.xml, struts-config.xml, yan.xml и так далее. Часто ошибки именно в неправильной конфигурации.
Поэтому пример, написанный один раз работающим, дальше кочует по другим частям программы слегка адаптируясь под контекст. Например, у нас есть сервер и для него конфиги просто копируются из одного места в другой будучи чуть-чуть подправленными под свои нужды. Написание данного конфига — это давно уже тема для внутрикорпоративного фольклора.
Так вот, чтобы протестировать код, нужно написать правильный конфиг, ну а дальше вы поняли... Рекурсия, как всегда, божественна.
4. Писать ЮТ до того как не написано ничего по теме — это распылять драгоценное время.
Как правило проблема окончательно проясняется после частичного её решения — увы, такова жизнь. Лучше взять листочек с карандашом и порисовать стрелочки (то есть заняться проектированием) или не откладывая в долгий ящик почитать туториал для используемых библиотек и поковыряться в примерах (таким образом получая важную информацию "снизу", необходимую для проектирования).
В данном случае уже работает выбор худшего из двух зол. Что хуже: ошибка в архитектуре или ошибка в работе метода? Я думаю, что как правило первое хуже.
Удар от отсутствия юнит-тестов смягчает наличие другого тестирования, типов и IDE. Вдобавок, если юнит-тесты будут, то не будет (или будет в недостаточном количестве) чего-то другого. Как всегда, бесплатный сыр соблазнительно качается на пружинке...
Подозреваю, что меня закидают тухлыми помидорами, но я рискну
5. Статическая типизация в сочетании с IDE работает удовлетворительно и без тестов.
Я отнюдь не противник динамически типизированных языков. В частности, мне очень интересен Erlang, и динамическая типизация в нём — то что доктор прописал. Действительно, взгляните на этот код:
allocate([R | Free], Allocated, From) ->
log({'allocate...', [R | Free], Allocated}),
From ! {resource_alloc, {yes, R}},
log({Free, [{R, From} | Allocated]}),
server(Free, [{R, From} | Allocated]);
allocate([], Allocated, From) ->
From ! {resource_alloc, no},
server([], Allocated).
Какие типы должны иметь "объекты" для функции allocate? Это очевидно. Например, первый аргумент должен иметь такой тип, чтобы он вёл себя как список, причём если он не пустой, то его можно будет передать в функцию log, затолкать его первый элемент в кортеж вместе с атомом "yes", и послать другому процессу и т.д.
Фактически я только что рассказал, как будет использоваться этот аргумент в функции. И если мне нужно описать этот тип, то это описание будет фактически повторять то, как он (тип) будет использоваться. То есть дублировать уже то, что написано в самой функции.
Может этот тип ещё где понадобится? Для Эрланга это нехарактерно — каждая функция уникальна, и следовательно аргументы используются уникальным образом. Тип как будто вспыхивает на мгновение при входе в функцию и исчезает после выхода из функции.
Для Явы однако всё по-другому. Какой-нибудь InputStream может быть воткнут повсюду в программе и в каждой точке маскировать разные объекты. Вот теперь я попытаюсь ответить на вопрос, почему статическая типизация работает (хоть и не даёт фантастических результатов).
Вся программа состоит из выражений, где пожалуй особняком стоит вызов методов. Почему?
Работа программы определяется правильным состоянием в каждый момент времени, которое в свою очередь определяется правильным состоянием каждого объекта, поэтому я придаю такое значение именно методам как формирующим
чужое состояние. Своим состоянием управлять более-менее научились (в смысле своего класса), а вот чужое может вызвать много всяких эффектов, ньюансы чужого состояния постоянно ускользают из внимания. Так вот, статическая типизация помогает правильно управляться с чужим состоянием.
Подвожу итог.
В одном предложении: вопрос написания ЮТ — это вопрос расстановки приоритетов.
В условиях ограниченного времени приходится выбирать наиболее приоритетные задачи. Поскольку работоспособность кода напрямую зависит от архитектуры, понимания проблемы и эффективного использования окружения, то время выделяется прежде всего на эти задачи. И так как итоговый результат получается удовлетворительный, то юнит-тесты выпадают из рассмотрения. Возможно такие быстрые проекты нужно писать на других языках (ФЯ или тех же динамических) или другим способом (XP, TDD), но без обкатки это большой риск.
Чем в данных условиях могут помочь юнит-тесты? А ничем. Но так как умные сволочи вроде Эккеля, Фаулера и Бека говорят, что "если юнит-тестов нет, то программа не работает", то у меня закрадывается сомнение — может мы действительно делаем вещи неправильно?
PS: Текст получился великоват, поэтому спасибо за уделённое внимание.
Минусы? Тож спасибо, только маленькая просьба: укажите место с чем именно не согласны.