Ваш так называемый TDD действует как опий: он завлекает и приглушает боли вместо того, чтобы придать силы.
(сказал бы Новалис, если бы жил сейчас)
Один пацан писал все через TDD, говорил что нравится, удобно, читабельно.
Потом его в дурку забрали, конечно.
В последнее время все громче и настойчивее звучат голоса о пользе TDD вообще и главенствующей роли unit тестов в частности. Правда радует, что пока еще многие говорят, что делают это, но мало кто занимался им на самом деле. Но тенденция пугает, в первую очередь потому, что идеология выстроена с вкраплением здравых и полезных мыслей, из которых потом делают странные выводы и обобщения.
Да я согласен, что тесты писать хорошо, но покрывать ими абсолютно весь код - плохо. Я считаю unit тесты полезными, но возводить их в абсолют - кажется мне странной затеей. Заменять документацию только тестами - глупо. Избегать функциональных тестов, только потому что они медленнее и сложнее - неправильно.
Вот тут есть обстоятельное видео от Андрея Солнцева, с которым я бы рекомендовал ознакомиться, там, особенно в первой части изложено довольно много концепций и аргументов защитников TDD и околотддшных практик, поскольку сейчас излагать их все последовательно я бы не хотел.
TDD - наглядная агитация
Наверное первым приводят довод про то, что писать через TDD легко и просто, волосы сразу становятся мягкими и шелковистыми, красноглазие проходит, а зарплата начинает стремиться в бесконечность. А что бы после этих заявлений их не сочли шарлатанами, являют миру небольшое чудо. Оно приходит к нам в виде маленького примерчика на пару тройку десятков строк кода, типично - калькулятора или чего-то похожего, обязательно с предельно простой логикой, без лишних зависимостей и как бы случайно - легко тестируемого. На прямые вопросы неверующих о тестировании чего-то более менее реального - многопоточного допустим, в лучшем случае отделываются общими фразами и отсылают к священным писаниям по TDD, рассказывая про уникальность каждой
Первые пару раз, должен признать это производит эффект. И на волне полученного адреналина идешь творить. Конечно же по заветам TDD. Но поскольку это не презентация, то и задача выбирается первая попавшаяся, не заточенная под TDD - ну допустим менеджер потоков. Но как-то блин unit-тесты выходят сложнее чем сам менеджер, совсем не так как у дяди в презентации, который обещал сладкой жизни. А вот допустим на код инжекта в системный сервис - как вообще тесты написать, да еще до кода? Нет, не выходит каменный цветок. Еще пяток другой экспериментов с переменным успехом и постепенно приходит понимание, что unit тесты ограниченно применимы, иногда вредны, а TDD в этом плане вообще зачастую не жизнеспособен. А фразам о том, что лишь познавшие дзен, после многих лет способны на код через ТДД - просто перестаешь верить.
Я не то что бы не понимаю основной идеи, она мне даже импонирует: придумал сложный use case использования кода, написал на него тесты и выкинул из головы эту проблему. Но делать это с каждым куском тривиального кода, причем делать еще до написания этого кода - нет уж увольте. Да бывают тяжелые куски логики, с массой возможных исходов, там без тестов никак. и если там удалось предварительно все до деталей проработать, составлены всякие UML диаграммы, блок схемы кода и еще фиг знает что, то можно в качестве развлечения даже тесты написать раньше кода. Но нужно понимать, что бенефиты дает детальная проработка задачи, но никак не написанный заранее тест.
Тесты как документация
Отличная мысль, нет правда, когда нет нормальной документации, разбирать функционал по тестам, особенно если они правильно написаны довольно легко. Но загвоздка в том, что по крайней мере unit тесты подразумевают множество тестов на каждую функцию. Мы проверяем "основной" путь, потом нам нужно покрыть каждую ветку кода, потом граничные условия, потом передачу некорректных данных функции…, в итоге разобраться в хитросплетении десятков тестов на одну бедную функцию - легче застрелиться. И никакие названия тестов тут не спасут, даже на чистом русском, даже если с юмором. Когда код тестов в десятки раз больше размеров тестируемого кода - легче прочитать и понять код.
Тест вообще говорит только о том, что именно в этом конкретном случае, с этими конкретными данными код ведет себя вот так. Все! Что бы узнать как работает код в общем случае - нужно, как ни странно, посмотреть на код.
А тем кто все же сомневается и считает тесты хорошей альтернативой документации, рекомендую попробовать поизучать boost, по их тестам. Для не любителей плюсов, судя по отзывам, хорошей альтернативой будет spring framework.
Правда тут есть исключения, иногда по коду не понятно, сделал ли это программист специально, защищаясь от каких-то побочных эффектов или еще чего или просто сглупил. Тогда стоит заглянуть в тесты, если он осознанно делал какой-то участок кода, который очевидно может вызывать вопросы, он скорее всего это зафиксировал в тестах, что бы шаловливые ручки последующих поколений не зарефакторили что не надо.Но такие случаи скорее исключение, чем правило.
100% покрытие
Еще один странный постулат, продиктованный скорее прогрессирующим перфекционизмом и выбором не кошерных языков программирования, чем необходимостью. Пишут тесты и не могут остановиться, пишут на каждый сеттер, геттер, покрывают вызов каждого исключения, ни единой строчки кода без теста. Страдают этим обычно пишущие на динамических языках - типичный представитель - js, там где можно очень быстро писать over тысячу знаков в минуту, "но такая фигня получается". Что ожидаемо, ведь пока каждый оператор в коде не дернуть из тестов, быть уверенным даже в правильности синтаксиса - нельзя.
В языках старой школы, так нелюбимыми хипстерами, с этим все гораздо лучше, компилятор предоставляет определенные гарантии и не даст вызывать допустим только что удаленный метод. Тут к месту будет упомянуть недавно вышедшего в релиз Rust, где в компилятор встроен весьма продвинутый анализатор кода, в том числе и актуального нынче многопоточного кода. "И увидел Он, что это хорошо" - действительно заставлять человека заниматься тем, что может делать тупая железяка - глупо, в добавок еще и дорого. Этим должны заниматься компиляторы, статические анализаторы кода, санитайзеры и другие инструменты анализирующие код в динамике (типа valgrind и т.п.) Еще раз обращу внимание - речь сейчас идет не о тестировании логики, а о тестировании того, от чего защищаются 100% покрытием - опечаток, ошибок памяти, многопоточности и т.д.
Хотя конечно у web-разработчиков с языком для фронтенда и выбора то особо нет, но они похоже этого не замечают и не страдают. Мало того они стремятся перенести свой слабо типизированный, динамичный язык, со специфичной системой наследования, созданный за неделю на коленке у Айка - java script на server side. Это наверное, что бы в 2 раза больше TDD в мире было. (прим. автора: извините, я не хотел обидеть поклонников nodejs, просто вырвалось, да и сразу - вот этот пост, кстати самый заплюсованный в категории nodejs на хабре, переводил не я)
Да, и что бы 2 раза не вставать замечу, когда тесты покрывают код целиком и полностью, не оставляя живого места, любая попытка изменения функции или ее интерфейса приводит к дикому баттхёрту. И никакая самая продвинутая IDE не поможет менять вслед за кодом тесты легко и просто. А что бы предупредить аргументы о том, что написанный по TDD код никогда переписывать не придется, перейдем к разбору следующего постулата.
С TDD сразу и навсегда
Следующий миф - TDD позволяет сформировать хорошую архитектуру на ранних этапах, которую потом почти не придется менять (как и код), ибо TDD заставляет подумать до написания кода. Этим же они оправдываются, в ответ на довод, что любой более менее серьезный рефакторинг приведет к переписыванию тысяч тестов.
У этих людей не уживается в голове одна простая мысль, что сделать идеально никогда не получится. И через месяц, два, год - вырастая профессионально, будешь смотреть на свой, написанный ранее код с улыбкой, понимая сколь много ты не учел. Если в вашем случае это не так, то у меня для вас плохие новости… И для тех, кто бегает с пеной у рта доказывая что с TDD они познают дзен и вот прям щас с первого раза напишут правильно у меня тоже плохие новости…
Да что там через месяц, два - зачастую на следующий день придя на работу, когда за ночь все в голове утряслось, появилось более глубокое понимание задачи, ты смотришь на свой код и понимаешь - в мусорку. Избранный подход не работает, надо переписать пока не поздно, оставить то, что уже написано - путь в никуда… а тут бах! на тебе! сотни тестов на любое изменение умирают краснея. Совесть взывает одуматься, ведь ты и так вчера пол дня писал не функциональность, а тесты, а тут берешь и все ломаешь. Менеджер, которого только месяц назад уломали на обязательный TDD для всей команды, недобро покачивает головой. Оправдания в стиле: неожиданные обстоятельства, вот только сейчас нюанс выяснился, невозможно было предусмотреть, никого конечно не трогают…
А ведь казалось бы такое простое простое решение проблемы - отложить написание тестов на чуточку попозже, когда модуль\класс немного стабилизируется. Но нет, нам обязательно расскажут, что это не работает, разработчик не найдет времени написать тест позже, он непременно найдет способ уклониться и вообще он ленивая скотина. Но если начать писать тесты до кода, то все меняется, лень пропадает, желания хоть отбавляй, человек просто преображается. Я кажется даже знаю причину, когда менеджер спрашивает такого разработчика через неделю после выдачи задачи про прогресс, а он отвечает, что почти все готово - половину тестов написал, осталось только вторая половина, ну и по мелочи - код, рефакторинг…, то под звереющим взглядом - лень куда-то пропадает.
Но вернемся к идеальному коду с первого раза. Я не хочу сказать, что концепция: N недель проектируем, а потом сели и все по проекту написали - совсем никогда не работает. Это работает, но для каких-то типовых решений, для десятого в вашей карьере интернет-банка, с парой уникальных фишек это наверное идеальное решение. Но разработка чего-то нового, по крайней мере нового для вас, требует эволюционного проектирования и соответственно регулярной переделки кода. Особенно во время первых итераций, пока грабли еще не натерли мозоль на лбу. Про эволюционное проектирование в свое время основательно писал Мартин Фаулер.
Юнит тесты хорошо, а другие плохо
Объясняют это обычно тем, что юнит тесты быстрые, как… (сами закончите в меру своей испорченности) и что важно запускаются даже на вершине Эвереста. И блин не поспоришь ведь, действительно быстрые (про Эверест правда не проверял). Только вот ведь в чем беда, тестируют они только небольшие и несвязанные, строительные кубики кода - там где ошибок в большинстве случаев и нет. Ошибки, по опыту, начинаются после того, как из этих кубиков начинаешь строить что-то большее. Вот тут и вылазят самые коварные, трудноуловимые баги с многопоточностью, с сетью, с БД, т.п. которые пока были замокированы вели себя естественно совсем ни так, как в реальности.
А справятся с такими, реальными, ошибками, которые зависят от состояния системы - помогают, ужасно медленные, идущие по 6 часов, плохо ложащиеся на TDD - функциональные (интеграционные) тесты. А что делать? Жизнь жестокая штука.
Как тут не вспомнить Станислава Лема с его "Суммой технологий", где он утверждал, что тело человека состоит из идеальных кирпичиков - клеток, почти лишенных каких-либо недостатков, а в сумме получается организм из почти сплошных недоразумений, подверженный куче архитектурных багов. Как будто по TDD делали.
TDD нам думать и жить помогает
Только с TDD ваш код станет правильным и начнет цвести и пахнуть. Без оного все конечно же будет печально и грустно. Посмотрим за счет чего это достигается. Первая мантра - если писать код по тестам, то код будет легко тестировать. Прям КО: если код писать по блок схемам, то составить блок схему по такому коду будет легко. Вот только легко тестируемый код не дает никаких гарантий качества получившегося приложения.
Тогда в ход пускаются следующие софистские трюки, в философию TDD добавляют подобные постулаты:
- перед тем как начать что-то писать нужно хорошо подумать, а потом разрабатывать через TDD
- пишите код по TDD и ни в коем случае не используйте глобальных переменных
- пишите код по TDD и старайтесь делать модули максимально независимыми
- ...
Ну и конечно же затем, как достижение TDD, преподносят то, что код написанный по этой концепции получается очень качественным, не содержит глобальных переменных, модули хорошо изолированы, а разработчики думают перед тем как написать код.
А вот без повторения слова ТДД вдолбить это в головы программистов никак нельзя? Вот прям если попытаться написать тест после кода, то в классе обязательно в каждом методе будет обращение к БД (причем через драйвер реализованный прям в этом же классе). Все переменные будут глобальными (ну у продвинутых небожителей возможно они будут завернуты в синглтоны). А логи будут писаться http запросами на захардкоженный внутри кода адрес, что ли?
По моему очень скромному мнению, этому всему можно научиться вне контекста этих трех волшебных букв.
Итоги кратко
- Тесты важные важны, тесты разные нужны.
- Когда писать тесты - личное дело каждого и на качестве это особо не сказывается, главное не забивать совсем.
- Тесты спорный суррогат документации.
- 100% покрытие или близкое к нему только мешает.
- Тесты далеко не единственный способ обеспечения качества кода, еще есть мозг, а так же статические и динамические средства проверки.