Ваш так называемый TDD действует как опий: он завлекает и приглушает боли вместо того, чтобы придать силы.
(сказал бы немецкий философ Фридрих Новалис, если бы жил сейчас)
Привет! Меня зовут Владимир, я работаю программистом в компании Quadcode. Вот уже почти полтора десятилетия я при помощи доброго десятка языков программирования разрабатываю приложения - от простых, вроде маленького плагина для Emacs, до сложных распределенных систем. Последние 4 года своей жизни я посвятил компании Quadcode, где занимаюсь разработкой транспортной подсистемы. Лет пять назад я вплотную столкнулся с адептами TDD (test-driven development) и это произвело на меня настолько сильное впечатление и оставило так много эмоций, что я написал “для своих” критический разбор наиболее часто встречаемых мною тезисов об этой технике (я бы даже сказал - учении). До сих пор мое мнение о TDD не изменилось, так что хотел бы описать его под катом и предлагаю обсудить вместе спорные моменты в комментариях.
Осторожно - TDD!
В последнее время все громче и настойчивее звучат голоса о пользе TDD. Правда радует, что пока еще многие говорят, что делают это, но мало кто занимался им на самом деле. Но тенденция пугает, в первую очередь потому, что сама идеология test-driven development выстроена с вкраплением здравых и полезных мыслей, из которых потом делают странные выводы и обобщения.
Да я согласен, что тесты писать хорошо, но покрывать ими абсолютно весь код - плохо. Я считаю unit тесты полезными, но возводить их в абсолют кажется мне странной затеей. Заменять документацию только тестами - глупо. Избегать функциональных тестов, только потому что они медленнее и сложнее - не очень правильно.
Вот тут есть обстоятельное видео от Андрея Солнцева, с этим роликом я бы рекомендовал ознакомиться, как с прекрасным примером пропагандистов технологии. В первой части изложено довольно много концепций и аргументов защитников TDD и околотддшных практик.
TDD — наглядная агитация
Если послушать адептов концепции, то получается, что TDD — некий философский камень в мире разработки. У всех, кто работает с TDD легко и просто, волосы сразу становятся мягкими и шелковистыми, красноглазие проходит, а зарплата начинает стремиться в бесконечность. При этом сторонники TDD отнюдь не голословны, после своих утверждений они обычно, являют миру небольшое чудо.
Обычно это небольшой пример на пару-тройку десятков строк кода, типично - калькулятора или чего-то похожего, обязательно с предельно простой логикой, без лишних зависимостей и как бы случайно (на самом деле нет) - легко тестируемого. На прямые вопросы “неверующих” о тестировании чего-то более менее реального - многопоточного допустим, в лучшем случае отделываются общими фразами и отсылают к священным писаниям по TDD, рассказывая про уникальность каждой твари Божьей конкретного случая.
Первые пару раз, должен признать, это производит эффект. На волне полученного адреналина идешь творить по заветам TDD. Но поскольку это не презентация, то и задача выбирается первая попавшаяся, не заточенная под TDD - ну допустим менеджер потоков. Но как-то блин unit-тесты выходят сложнее чем сам менеджер, совсем не так как у дяди в презентации, который обещал сладкую жизнь всем, кого удалось завлечь. А вот допустим на код инжекта в системный сервис - как вообще тесты написать, да еще до кода? Нет, не выходит каменный цветок. Еще пяток другой экспериментов с переменным успехом - и постепенно приходит понимание, что unit тесты ограниченно применимы, иногда вредны, а TDD в этом плане вообще зачастую нежизнеспособен. А фразам о том, что лишь познавшие дзен, после многих лет способны на код через TDD - просто перестаешь верить.
Я не то, чтобы не понимаю основную идею, совсем напротив. Она мне даже импонирует: придумал сложный use case использования кода, написал на него тесты и выкинул из головы эту проблему. Но делать это с каждым куском тривиального кода, причем еще до написания этого кода - нет уж увольте. Да бывают тяжелые куски логики, с массой возможных исходов, там без тестов никак. И если там удалось предварительно все до деталей проработать, составить всякие UML диаграммы, блок схемы кода и еще фиг знает что, то можно в качестве развлечения даже тесты написать раньше кода. Но нужно понимать, что бенефиты дает детальная проработка задачи, но никак не написанный заранее тест.
Тесты как документация
Тест вообще говорит только о том, что именно в этом конкретном случае, с этими конкретными данными код ведет себя вот так. Все! Чтобы узнать как работает код в общем случае - нужно, как ни странно, посмотреть на код.
А тем кто все же сомневается и считает тесты хорошей альтернативой документации, рекомендую попробовать поизучать boost, по их тестам. Для не любителей “плюсов”, судя по отзывам, хорошей альтернативой будет spring framework.
Правда тут есть исключения. Например, иногда по коду не понятно, сделал ли это программист специально, защищаясь от каких-то побочных эффектов или еще чего или просто сглупил. В этом случае стоит заглянуть в тесты. Если разработчик осознанно делал какой-то участок кода, который очевидно может вызывать вопросы, он скорее всего это зафиксировал в тестах, чтобы шаловливые ручки последующих поколений не зарефакторили что не надо. Но такие случаи скорее исключение, чем правило.
100% покрытие
Еще один странный постулат, продиктованный скорее прогрессирующим перфекционизмом и выбором некошерных языков программирования, чем необходимостью. Пишут тесты и не могут остановиться, пишут на каждый сеттер, геттер, покрывают вызов каждого исключения, ни единой строчки кода без теста.
Страдают этим обычно пишущие на динамических языках - типичные представители - jspython. Что вполне ожидаемо, ведь пока каждый оператор в коде не дернуть из тестов, быть уверенным даже в правильности синтаксиса - нельзя. Справедливости ради, типизация и продвинутые линтеры постепенно проникают и сюда, делая жизнь проще.
В языках старой школы с этим все гораздо лучше. Там компилятор предоставляет определенные гарантии и не даст вызывать, например, только что удаленный метод. Тут к месту будет упомянуть набирающий популярность Rust, где в компилятор встроен весьма продвинутый анализатор кода, в том числе и актуального нынче многопоточного кода. "И увидел Он, что это хорошо" - действительно, заставлять человека заниматься тем, что может делать тупая железяка - глупо, а вдобавок еще и дорого. Этим должны заниматься компиляторы, статические анализаторы кода, санитайзеры и другие инструменты анализирующие код в динамике (типа valgrind и т.п.). Еще раз обращу внимание - речь сейчас идет не о тестировании логики, а о тестировании того, от чего защищаются 100% покрытием - опечаток, ошибок памяти, многопоточности и т.д.
Да, и что бы 2 раза не вставать замечу, что когда тесты покрывают код целиком и полностью, не оставляя живого места, любая попытка изменения функции или ее интерфейса приводит к дикому баттхёрту. И никакая самая продвинутая IDE не поможет менять вслед за кодом тесты легко и просто. А чтобы предупредить аргументы о том, что написанный по TDD код никогда переписывать не придется, перейдем к разбору следующего постулата.
С TDD сразу и навсегда
Следующий миф – TDD позволяет сформировать хорошую архитектуру на ранних этапах, которую потом почти не придется менять (как и код), ибо TDD заставляет подумать до написания кода. Этим же сторонники технологии оправдываются в ответ на довод, что любой более менее серьезный рефакторинг приведет к переписыванию тысяч тестов.
У адептов TDD не уживается в голове одна простая мысль: сделать идеально никогда не получится. И через месяц, два, год - вырастая профессионально, будешь смотреть на свой, написанный ранее код с улыбкой, понимая сколь много ты не учел. И никакая методология, будь то TDD или что-то ещё не позволит познать дзен и с первого раза написать правильно.
Да что там через месяц, два - зачастую на следующий день придя на работу, когда за ночь все в голове утряслось, появилось более глубокое понимание задачи, ты смотришь на свой код и понимаешь - он никуда не годен. Избранный подход не работает, надо переписать пока не поздно, оставить то, что уже написано - путь в никуда… а тут бах! на тебе! сотни тестов на любое изменение умирают краснея. Совесть взывает одуматься, ведь ты и так вчера полдня писал не функциональность, а тесты, а тут берешь и все ломаешь. Менеджер, которого только месяц назад уломали на обязательный TDD для всей команды, недобро покачивает головой. Никого не трогают оправдания в стиле: "неожиданные обстоятельства, вот только сейчас нюанс выяснился, невозможно было предусмотреть".
Казалось бы - такое простое простое решение проблемы - отложить написание тестов на более позднее время, когда модулькласс немного стабилизируется. Но нет, нам обязательно расскажут, что это так не работает, разработчик не найдет времени написать тест позже, он непременно найдет способ уклониться и вообще он ленив. Но если начать писать тесты до кода, то все меняется, лень пропадает, желания хоть отбавляй, человек просто преображается. Я, кажется, даже знаю причину, когда менеджер спрашивает такого разработчика через неделю после выдачи задачи про прогресс, а он отвечает, что почти все готово - половину тестов написал, осталось только вторая половина, ну и по мелочи - код, рефакторинг…, то под звереющим взглядом - лень куда-то пропадает.
Но вернемся к “идеальному коду с первого раза”. Я не хочу сказать, что концепция “N недель проектируем, а потом сели и все по проекту написали”- совсем никогда не работает. Это работает, но для каких-то типовых решений, для десятого в вашей карьере интернет-банка с парой уникальных фишек это, наверное, идеальное решение. Но разработка чего-то нового, по крайней мере нового для вас, требует эволюционного проектирования и соответственно регулярной переделки кода. Особенно во время первых итераций, пока грабли еще не натерли мозоль на лбу, образно говоря. Про эволюционное проектирование в свое время основательно писал Мартин Фаулер.
Юнит тесты хорошо, а другие – плохо
Объясняют это обычно тем, что юнит тесты быстрые, как… (сами закончите в меру своей испорченности) и могут запускаться и работать даже на вершине Эвереста. И, блин, не поспоришь - действительно быстрые (про Эверест правда не проверял). Только вот ведь в чем беда - тестируют они только небольшие и несвязанные, строительные кубики кода - там где ошибок в большинстве случаев и нет. Ошибки, по опыту, начинаются после того, как из этих кубиков начинаешь строить что-то большее.
Вот тут и появляются самые коварные, трудноуловимые баги с многопоточностью, с сетью, с БД, т.п. которые пока были замокированы вели себя естественно совсем не так, как в реальности.
А справятся с такими ошибками, которые зависят от состояния системы - помогают функциональные (интеграционные) тесты. Они ужасно медленные, выполняющиеся по многу часов, плохо ложащиеся на TDD. А что делать? Жизнь жестокая штука.
Как тут не вспомнить Станислава Лема с его "Суммой технологий", где он утверждал, что тело человека состоит из идеальных кирпичиков - клеток, почти лишенных каких-либо недостатков, а в сумме получается организм из почти сплошных недоразумений, подверженный куче архитектурных багов. Как будто по TDD делали.
TDD нам думать и жить помогает
Еще один тезис адептов: “Только с TDD ваш код станет правильным и начнет цвести и пахнуть“. Без оного все конечно же будет печально и грустно”. Посмотрим, за счет чего это достигается.
Первая мантра – если писать код по тестам, то код будет легко тестировать. Прям КО: если код писать по блок схемам, то составить блок схему по такому коду будет легко. Вот только легко тестируемый код не дает никаких гарантий качества получившегося приложения.
Тогда в ход пускаются следующие софистские трюки, в философию TDD добавляют подобные постулаты:
перед тем как начать что-то писать нужно хорошо подумать, а потом разрабатывать через TDD
пишите код по TDD и ни в коем случае не используйте глобальных переменных
пишите код по TDD и старайтесь делать модули максимально независимыми
...
Ну и, конечно, как достижение TDD, преподносят то, что код, написанный по этой концепции, получается очень качественным, не содержит глобальных переменных, модули хорошо изолированы, а разработчики думают перед тем как написать код.
А вот без повторения слова TDD вдолбить это в головы программистов никак нельзя? Вот прям если попытаться написать тест после кода, то в классе обязательно в каждом методе будет обращение к БД (причем через драйвер реализованный прям в этом же классе). Все переменные будут глобальными (ну у продвинутых небожителей возможно они будут завернуты в синглтоны). А логи будут писаться http запросами на захардкоженный внутри кода адрес, что ли?
По моему очень скромному мнению, этому всему можно научиться вне контекста этих трех волшебных букв.
Итоги кратко
Тесты важные важны, тесты разные нужны.
Когда писать тесты - личное дело каждого и на качестве это особо не сказывается, главное не забивать совсем.
Тесты - спорный суррогат документации.
100% покрытие или близкое к нему только мешает.
Тесты далеко не единственный способ обеспечения качества кода, еще есть мозг, а также статические и динамические средства проверки.
А что думаете насчет TDD вы? Давайте обсудим в комментариях. Возможно, эта технология все же очень важная и нужная?