Comments 60
TDD хорош, когда тесты можно написать до программы. Очень часто программирование exploratory, т.е. "что получится никто не знает". В этой ситуации игра в TDD это глупость.
А бывает так, что есть совершенно точное ТЗ к которому можно написать тест до кода, и даже можно написать проходящий тест мок (на одном наборе данных), а потом можно писать код.
Это совершенно разные задачи — одно (exploratory) это элемент НИОКР, а второе — просто кодинг. Вот для задач кодинга TDD кратно повышает качество кода. Для НИОКР качество кода вторично по сравнению с нахождением решения, так что TDD лишь мешает искать решение.
Вот и всё.
Это вы описываете как вы решаете простые задачи простыми методами. Бывает так, что результат exploratory programming не ясен до самого конца, потому что есть хаотические факторы (нагрузка, конкурентность, маштаб, невидимые обстоятельства-состояния). Бывает так, что даже задача не ясна "я хочу примерно вот так… или лучше вот так". Отказываться от такого — лишать продукт права на развитие. (Да, мы все умеем писать TDD для CRUD. Слабо написать тесты для ещё ненаписанной программы, которая делает удобно при редактировании текста?)
Повторю тезис: TDD хорош для задач кодинга, когда не нужно исследовать.
Прямо из бэклога:
Нужно переносить сервера (инстанса приложения) между разными кластерами в разных стойках (т.е. с разными Top-of-Rack свитчами) с минимальным даунтаймом.
Это, кстати, почти контр-пример для моего тезиса, потому что тест для определения даунтайма я могу с лёгкостью написать даже не зная как я буду реализовывать её.
… Давайте более exploratory.
Я хочу в терминале возможность прыгать между вызовами команд. Т.е. отдельный комплект хоткеев, который позволяет переходить между строчками, где начинается вывод от новой команды с шелла.
Ну какие тут TDD?
Можете пояснить чуть более подробно?
Формат строчек не известен.
Вот у меня шелл на удалённый сервер:
$ dch -v 5.8.7-2
$ git diff
$ git add debian/changelog
$ git commit --amend
$ git push
Между ними вывод. Я хочу между ним прыгать. Вывод зависит от настроек шелла на удалённом сервере и заранее его угадать нельзя. Вывод программ может включать себя команды (например, cat ~/.bashrc).
Хз как делать. Но было бы чертовски удобно.
Наверное, я бы придумал сделать esc код для терминала (который мой терминал понимает) и передавать его в PS'е. Или придумать новый тип для TERM. Или нужно написать свой хук в башовый PS для каждого сервера.
Короче, как делать не понятно, но очень хочется попробовать. Если с подсказками не получится, возможно, можно использовать тайминги. Пользователь печатает в шелле — значит, ввод.
Или прям нейронную сеть учить.
Вот это пример exploratory programming. Пойти туда, не знаю куда, принеси мне Фичу.
А я в данном примере вообще не вижу целесообразности тестов. Потому что простые случаи легко покрыть прямо запусками из терминала, а для внезапных граблей никакой мегамозг тест не напишет, — до того, как на грабли эти наткнется.
Если можно менять PS1
— задача вырожденно-тривиальная. Если нельзя — в общем случае не решаемая, потому что удаленный шелл не нанимался помнить историю вывода и туннели через 2+ ssh
все выкрутасы с радостью похерят.
Если я хочу это в своем уютненьком терминале на лаптопе — тут вообще делать нечего, Enter
перехватить и ага.
Но правильное решение такой задачи внезапно тривиально: хоткей, который маппится на «поиск назад строки, содержащий текущий промпт». Будут ложные срабатывания? — Да, возможно. Критично? — Вот вообще нет. Хоткеями пользуются люди, нажмут еще раз, если вдруг что.
Писать на такую задачу тесты — это вообще себя не уважать.
Я думаю, если речь идет о платном приложении, нестабильность горячих клавиш может легко стать причиной неуспеха.
Я вот очень сильно переживаю, если у меня хоткеи не работают надежно.
И не тестировать такой функционал — это не уважать пользователя, а писать ненадежный софт — не уважать себя. Разве нет?
Есть отличная глава "QA should find nothing" из книги Clean Coder.
Больше всего в современном состоянии CS меня напрягает бесконечное обилие никому не нужных методологических книг на фоне абсолютного отсутствия руководств по таким насущным вопросам, как умение понимать поставленную задачу.
Платное приложение? С гарантией работы горячих клавиш? Ну, допустим. Это совершенно не та задача, которую мы тут обсуждаем, но допустим.
Начните с написания терминального клиента. При установке соединения с удаленным хостом, загрузите туда свой код, который перехватит запуск из шелла и будет по определенному вами бинарному протоколу в отдельном канале присылать вам метаинформацию. Обмажьте это тестами (а если ваша команда упоролась по хайпу — то и типами) со всех сторон.
Настройте стенд с матрицей всех шеллов всех версий, популярность которых выше 0.1%. Прогоните все тесты там.
Можно запускаться.
Есть отличный способ изюежать необходимости читать много водянистой беллетристики: думать своей головой.
Ну вот у меня например задача была — закодить систему генерации городской застройки. Т.е. даешь ей полигон участка, она сама раскидывает там домики и дороги, с учетом градостроительных норм и всяких принципов хипстоурбанизма.
Ну и я вот месяц сидел и фигачил туда-сюда всякие алгоритмы расстановки дорог и домиков.
Тут во-первых непонятен результат (ну то есть хочется получить красивую застройку, но непонятно даже как именно она должна выглядеть, это плохо формализуется, а самих норм недостаточно). Во-вторых надо очень быстро и много чего пробовать и менять — от тестов тут толку особо нет, так как их придется постоянно переписывать, по мере смены концепций и алгоритмов.
Вот когда уже получен хороший результат, и теперь надо на его основе сделать устойчивый продукт — там уже можно написать тесты, а потом начать разгребать всю ту кучу говнокода, которая родилась в процессе исследований и прототипирования.
В классическом TDD тест — это Unit-test, а тестируемый объект это метод, или даже отдельная ветка исполнения в методе. Готов поспорить, что на этом уровне в любом приложении будут детерминированные результаты.
И тесты сразу становятся интеграционными, и перестают работать приемлемо быстро. Это полностью лишает смысла применять такие тесты в методологии типа TDD, когда быстрый ответ важен.
Но аргументы, написанные выше, не столько против TDD, сколько против тестов вообще в определённых случаях.
Дело в том, что основной смысл в тестах — это фиксация поведения программы при определённых сценариях, чтобы не допустить регрессий при доработке программы. Также тесты играют роль документации. При это тесты не гарантируют корректность работы программы. Тесты, как и код, могут содержать ошибки. Тесты могут быть неполными даже при TDD.
Нужно ли писать тесты, если задача поисковая? Моё мнение — нет, потому что поведение программы постоянно меняется, а тесты, наоборот, его фиксируют. То есть придётся делать двойную работу: переписывать и код, и тесты. А если это обработка изображений, так затраты на тесты там вообще на порядки выше, чем на написание кода. Работоспособность алгоритмов намного проще проверить глазами.
Ну то есть тут уже всё становится индивидуально. Есть люди, кому проще писать код сразу с тестами — ок, пусть пишут. А если люди, кому это в тягость — пусть не пишут.
Т.е. мы хотим получить какой-то результат (как его получить мы не знаем).
В тестах мы фиксируем результат.
А потом последовательными итерациями к нему приближаемся.
Или мы «тестируем гипотезу».
Опять же в тестах мы формулируем гипотезу и смотрим, что получается/не получается.
Это отдельная грань проблемы, которую нужно исследовать.
TDD хорош, когда тесты можно написать до программы.
Кстати, да. У меня были случаи, когда еще на этапе написания тестов по ТЗ становилось понятно, что в ТЗ ошибка.
Я бы добавил ещё один случай — при написании кода, главное в котором:
- Скорость выполнения
- Работа с файловой системой/сетью/другим железом
Юнит тесты — не особо подходящий инструмент.
Делать методы открытыми, а тем более — заводить интерфейсы ТОЛЬКО для юнит тестов — такое решение может привести к кардинальной деградации производительности.
Если тестировать интеграционниками с использованием не моков, а тестового окружения — то такой «не совсем TDD» может оказаться неплох.
Но если добавить в винегрет ещё и исследования (а при написании тайм-критикал кода они всегда есть), то всё-таки лучше писать тесты после кода.
Я вам вполне могу написать TDD для интеграционных тестов, это не проблема. Даже с учётом скоростей. Вы путаете деление юнит-тесты/интеграционные тесты (что на самом деле вопрос про размер сайд-эффектов) и вопрос "код вперёд или тесты вперёд".
Тут вопрос "а знаем ли мы заранее что мы пишем?" или нет.
Вы путаете деление юнит-тесты/интеграционные тесты (что на самом деле вопрос про размер сайд-эффектов) и вопрос «код вперёд или тесты вперёд».
«Тесты вперёд»(test first) и TDD это разные вещи, и TDD про написание конкретно юнит-тестов.
Ой ли? TDD ровно так же применим для интеграционных тестов, как и для юнит-тестов. Я бы сказал, что он даже более применим, потому что сайд-эффекты обычно проще придумать (до написания кода), чем потрошки интерфейсов.
Но зачастую сложнее проверить. Скажем, у нас эффект выполнения программы в целом — это создание каких-то файлов, или скажем запись чего-то в базу. Не то чтобы это было невозможно проверять, но трудоемкость таких тестов сопоставима с трудоемкостью написания основного кода — ну и понятно, что производительность это серьезно снижает.
>TDD ровно так же применим для интеграционных тестов
Ну то есть, наверное где-то применим — но может быть ужасно неудобно, если у вас весь код состоит из интеграций. Пробовал я на примере ESB такое делать… мок на моке сидит, и моком погоняет. А что мы протестировали — очень быстро перестаешь понимать.
Интеграционные тесты единичные, но очень сильные. (и медленные). Я к тому, что для хорошо сформулированной задачи TDD для интеграционных тестов может быть даже более разумным, чем для unit. Чёрный ящик в чистом виде, пиши как хочешь, но тесты должны пройти. При этом сами тесты завязаны на сайд-эффекты, то есть моками их не обманешь.
Утрируя: у нас интеграционный тест для замка с удалённым открыванием. Интеграционный тест выглядит как кронштейн для карточки и тиски для замка, плюс проверка "открылось или нет" (посредством замыкания контакта на приёмнике языка замка).
Дальше вы можете использовать любые методы, но с правильным ключом оно должно открыть, а с неправильным — не открыть. А уж монадки там или ассемблер уже не важно.
И такой тест можно реализовать до того, как будет написана даже первая строчка кода для прошивки замка. И он не поменяется даже если вы отрефакторите всё и вся (кроме форм-фактора самого замка).
Есть ещё один момент. Бывает так, что не совсем понятно, какую задачу решаем. Т.е. есть интуитивное ощущение "сделать лучше", но как именно — не понятно. И пока не напишешь, понятно не станет. Именно так появляется инновационный (в смысле, "новый в своём классе") софт.
Т.е. мы не знаем точный конечный результат, но в процессе декомпозиции на каждом этапе мы точно знаем, какой конкретно результат хотим получить.
Фиксируем в тесте «гипотезу» (необходимый результат).
А дальше пишем код под эту гипотезу.
Либо гипотеза подтверждается, и мы можем получить необходимый результат.
Либо нет, тогда формируем другую гипотезу.
После прочтения исследований, у меня есть только один логичный ответ — эффективность и применимость TDD зависит, прежде всего, от конкретного разработчика
Эффективность и применимость TDD зависит, прежде всего, от предметной области и особенностей решаемой задачи. О чём выше уже сказали.
У вас есть какие-нибудь научные обоснования, или это основано на Вашем опыте?
В любом случае — гипотеза принята к рассмотрению, спасибо.
Вся ваша статья вроде бы о том, что научные обоснования — не обоснования. :)
Моё мнение основано на личном опыте и наблюдении за происходящим в проектах, в которых я участвовал/участвую. Это в основном R&D проекты, зачастую без конкретных ТЗ и спецификаций. Несколько проектов переписывались с нуля несколько раз, всё что создавалось на начальных этапах просто выбрасывалось.
Кстати, тот же спор можно развязать о применимости Agile и прочих скрамов.
Я сторонник разумного подхода. Если в конкретном проекте или части проекта на конкретном этапе его разработки видно, что можно применить TDD, значит нужно попробовать его использовать, хуже точно не будет. Если я вижу, что вот этот конкретный модуль/функция точно будет именно таким и я могу сразу специфицировать его логику и входы/выходы, я сразу пишу тесты.
Вся ваша статья вроде бы о том, что научные обоснования — не обоснования. :)
Не совсем, скорее о том, что у нас нет (пока?) убедительных доказательств любого из утверждений:
1) TDD эффективен
2) TDD не эффективен
3) TDD не оказывает влияния на эффективность
И, соответственно, нужны еще исследования.
Но, насколько я пониманию
Несколько проектов переписывались с нуля несколько раз, всё что создавалось на начальных этапах просто выбрасывалось.
никак не противоречит применимости TDD, т.к. изменение самих тестов является одним из возможных шагов в «цикле» TDD. С учетом того, что фокус Unit Test это метод или даже определенная ветка в методе, наличие Т.З. не должно оказывать существенного влияния.
Нужно ли лучше формализовать понятие TDD?
Почему-то этот тезис используется для подкрепления полезности TDD. Но я не вижу логики в таких рассуждениях. Никто мне не мешает сделать изначально всё хорошо, а потом написать тесты. Или сделать криво, иметь проблемы с написанием тестов, поправить интерфейсы и зависимости и успешно написать тесты. Тесты помогают, да. Почему их надо писать перед написанием кода? Не знаю.
1. Написали какой-то скелет. Архитектура, интерфейсы устаканились.
2. Написали юнит-тесты.
3. После этого желательно держать уровень покрытия тестами не ниже какой-то планки относительно текущего уровня.
4. Потом, для новой функциональности уже можно делать Test-First или Test-Last. Не уверен, что это имеет большое значение. Имеет значение наличие хороших тестов.
При TDD тесты являются формальным описанием (микро)задачи. Таким образом, как только тест становится «passed» мы можем сделать вывод, что (микро) задача выполнена.
Также, при таком подходе исчезает соблазн «подгонять» тесты под код (а то будет как в лабораторных по теории цепей в старом, добром университете — "пофиг что намеряли, пиши что соответствует заданию, а то все поймут что мы цепь хреново сделали")
Ну, по крайней мере, такая изначальная гипотеза
На самом деле можно выделить не две, а три модели предметной области:
- модель в коде функциональности;
- модель в коде тестов;
- модель в голове разработчика.
Это отличная цитата из статьи, но не противоречит ли Ваше высказывание Вашей же статье?
Даже если мы признаем утверждение
Тесты — это хороший пример дублирования, метода повышения надёжности за счёт реализации нескольких копий критической части системы. Нюанс заключается в том, что в нашем случае через дублирование контролируется не работа результирующей системы, а точность её формальной модели.
истинным (оно, как минимум, правдоподобно), это, тем не менее, не исключает возможности влияния тестов на качество продукта.
Более того, пропоненты TDD, как раз считают, что смысл TDD не в тестах, а в понимании и точности модели.
Ваш ход?
не противоречит ли Ваше высказывание Вашей же статье?
А в чём конкретно противоречие?
Я стою на том, что тесты нужны как хороший способ гарантии качества через дублирование, независимо от порядка их написания. В каком порядке писать зависит от привычек и тараканов разработчиков, от предметной области.
не исключает возможности влияния тестов на качество продукта.
Я нигде не утверждал о каком-либо исключении влияния на качество. Собственно, из цитаты (и статьи) влияние на качество непосредственно следует.
Более того, пропоненты TDD, как раз считают, что смысл TDD не в тестах, а в понимании и точности модели.
Ошибка выжившего.
Те, кому легче формировать понимание предметной области тестами, используют TDD по факту своей работы. Они бы его использовали, даже если бы такого названия не было.
Тем, кому легче разбираться с предметной областью не через тесты, TDD мешает и они от него отказываются.
Ошибка выжившего.
Те, кому легче формировать понимание предметной области тестами, используют TDD по факту своей работы. Они бы его использовали, даже если бы такого названия не было.
Тем, кому легче разбираться с предметной областью не через тесты, TDD мешает и они от него отказываются.
Абсолютно согласен, это по сути — моя гипотеза, но иными словами. Ваша формулировка даже более удачная!
Уточнить требования во время написания тестов получится быстрее
Не согласен. Почему?
раннее прототипирование очень полезно.
Согласен, но раннее прототипирование — это не написание тестов перед кодом. Прототип может быть любым: кодом, тестами, моделью в специализированном софте, ролевой игрой с живыми участниками.
Своему коллеге вы предлагаете оценить по тестам вашу реализацию моделей.
Или по коду. Не вижу проблемы.
Суждения о валидности этой модели по отношению к изначальным требованиям не могут быть поревьюены по истории коммитов. Потребуется чтение ТЗ.
Не понял про что это утверждение.
В случае TDD можно посмотреть эволюцию формализации требований.
Эфолюцию формализции требований можно посмотреть по чему угодно, что находится под контролем версий.
Эфолюцию формализции требований можно посмотреть по чему угодно, что находится под контролем версий.
Может ли предположить, что тесты выступают как ubiquitous language, понятный всем разработчикам\заинтересованным лицам?
Тесты будут понятны также как и основная логика, поскольку и то и другое — код. Читают код, который уже в голове интерпретируется в тесты или основную логику.
Можно приложить усилия и сделать тесты более понятными для части людей (визуализировав их как-то, например), но можно те же самые усилия приложить к коду и получить тот же эффект. Зависит от того, что выгоднее в данной команде на данном проекте.
Я, например, предпочитаю смотреть Pull Request начиная с тестов — так я могу понять чего пытались достичь, и более внимательно\продуктивно рассмотреть сам предложенный код.
Но я не могу исключить возможности, что этот метод работает не для всех.
Эффективен ли TDD?