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

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

TDD хорош, когда тесты можно написать до программы. Очень часто программирование exploratory, т.е. "что получится никто не знает". В этой ситуации игра в TDD это глупость.


А бывает так, что есть совершенно точное ТЗ к которому можно написать тест до кода, и даже можно написать проходящий тест мок (на одном наборе данных), а потом можно писать код.


Это совершенно разные задачи — одно (exploratory) это элемент НИОКР, а второе — просто кодинг. Вот для задач кодинга TDD кратно повышает качество кода. Для НИОКР качество кода вторично по сравнению с нахождением решения, так что TDD лишь мешает искать решение.


Вот и всё.

Вот и я сначала пишу какую-то небольшую часть кода, прикидываю, что получается, и когда картина более-менее ясна, дальше уже продолжаю кодить одновременно с написанием тестов. Здесь тесты очень помогают не сломать что-то очередными изменениями.

Это вы описываете как вы решаете простые задачи простыми методами. Бывает так, что результат exploratory programming не ясен до самого конца, потому что есть хаотические факторы (нагрузка, конкурентность, маштаб, невидимые обстоятельства-состояния). Бывает так, что даже задача не ясна "я хочу примерно вот так… или лучше вот так". Отказываться от такого — лишать продукт права на развитие. (Да, мы все умеем писать TDD для CRUD. Слабо написать тесты для ещё ненаписанной программы, которая делает удобно при редактировании текста?)


Повторю тезис: TDD хорош для задач кодинга, когда не нужно исследовать.

Формализуйте, пожалуйста, понятие exploratory programming? Можно конкретный пример? Я тогда бы учел это в исследовании, у меня, по ходу дела, отличная идея диссертации намечается.

Прямо из бэклога:
Нужно переносить сервера (инстанса приложения) между разными кластерами в разных стойках (т.е. с разными 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. Пойти туда, не знаю куда, принеси мне Фичу.

Так понятнее, спасибо. Действительно в данном примере я не вижу даже целесообразности TDD.

А я в данном примере вообще не вижу целесообразности тестов. Потому что простые случаи легко покрыть прямо запусками из терминала, а для внезапных граблей никакой мегамозг тест не напишет, — до того, как на грабли эти наткнется.


Если можно менять PS1 — задача вырожденно-тривиальная. Если нельзя — в общем случае не решаемая, потому что удаленный шелл не нанимался помнить историю вывода и туннели через 2+ ssh все выкрутасы с радостью похерят.


Если я хочу это в своем уютненьком терминале на лаптопе — тут вообще делать нечего, Enter перехватить и ага.


Но правильное решение такой задачи внезапно тривиально: хоткей, который маппится на «поиск назад строки, содержащий текущий промпт». Будут ложные срабатывания? — Да, возможно. Критично? — Вот вообще нет. Хоткеями пользуются люди, нажмут еще раз, если вдруг что.


Писать на такую задачу тесты — это вообще себя не уважать.

Я думаю, если речь идет о платном приложении, нестабильность горячих клавиш может легко стать причиной неуспеха.


Я вот очень сильно переживаю, если у меня хоткеи не работают надежно.


И не тестировать такой функционал — это не уважать пользователя, а писать ненадежный софт — не уважать себя. Разве нет?


Есть отличная глава "QA should find nothing" из книги Clean Coder.

Больше всего в современном состоянии CS меня напрягает бесконечное обилие никому не нужных методологических книг на фоне абсолютного отсутствия руководств по таким насущным вопросам, как умение понимать поставленную задачу.


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


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


Настройте стенд с матрицей всех шеллов всех версий, популярность которых выше 0.1%. Прогоните все тесты там.


Можно запускаться.




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

Ну вот у меня например задача была — закодить систему генерации городской застройки. Т.е. даешь ей полигон участка, она сама раскидывает там домики и дороги, с учетом градостроительных норм и всяких принципов хипстоурбанизма.


Ну и я вот месяц сидел и фигачил туда-сюда всякие алгоритмы расстановки дорог и домиков.


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


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

Ваш аргумент отлично работает против ATDD (Acceptance Test-driven development), но не TDD.

В классическом TDD тест — это Unit-test, а тестируемый объект это метод, или даже отдельная ветка исполнения в методе. Готов поспорить, что на этом уровне в любом приложении будут детерминированные результаты.
Детерминированные — да. Но это не всегда значит, что будет просто или быстро написать для них assert. Ну скажем, в моем случае, программа это spark (примерно тоже самое будет верно и для пандас, я думаю). Уже на самом верхнем уровне (собственно, там и кода может быть совсем мало) мы сталкиваемся с тем, что тестируемые объекты — это что-то, совершенно непригодное для юнит тестирования. Т.е. это датафреймы или датасеты, и чтобы они работали, нужно запустить собственно некий здоровый фреймворк.

И тесты сразу становятся интеграционными, и перестают работать приемлемо быстро. Это полностью лишает смысла применять такие тесты в методологии типа TDD, когда быстрый ответ важен.
Согласен, для таких задач Unit-Test слабо применимы. Хотя, есть примеры, когда в таких задача прекрасно работал Test-First подход с BDD фреймворками, например — www.youtube.com/watch?v=bny86gxbUcg
Отличие TDD от code-first заключается в том, что TDD принуждает к максимальному покрытию кода тестами и к написанию качественного кода. При использовании code-first велик соблазн оставить кусок кода непротестированным, ведь и так всё работает. При TDD такого не будет.

Но аргументы, написанные выше, не столько против TDD, сколько против тестов вообще в определённых случаях.

Дело в том, что основной смысл в тестах — это фиксация поведения программы при определённых сценариях, чтобы не допустить регрессий при доработке программы. Также тесты играют роль документации. При это тесты не гарантируют корректность работы программы. Тесты, как и код, могут содержать ошибки. Тесты могут быть неполными даже при TDD.

Нужно ли писать тесты, если задача поисковая? Моё мнение — нет, потому что поведение программы постоянно меняется, а тесты, наоборот, его фиксируют. То есть придётся делать двойную работу: переписывать и код, и тесты. А если это обработка изображений, так затраты на тесты там вообще на порядки выше, чем на написание кода. Работоспособность алгоритмов намного проще проверить глазами.

Ну то есть тут уже всё становится индивидуально. Есть люди, кому проще писать код сразу с тестами — ок, пусть пишут. А если люди, кому это в тягость — пусть не пишут.
Вот и мой основной тезис в том, что все очень индивидуально, и то, что работает для одного разработчика, может прекрасно не работать для другого
Прошу прощения, но ИМХО как раз тесты могут быть инструментом исследования!
Т.е. мы хотим получить какой-то результат (как его получить мы не знаем).
В тестах мы фиксируем результат.
А потом последовательными итерациями к нему приближаемся.
Или мы «тестируем гипотезу».
Опять же в тестах мы формулируем гипотезу и смотрим, что получается/не получается.

Мне нравится такой подход — "тест как гипотеза".
Ведь, в противном случае, как мы поймем, что мы наиследовали своим кодом? Разве что читать логи, что не всегда реально (если их тыщи)

Я думаю, что тезис «эффективность зависит от… разработчика» нужно просто расширить, например на инструмент (язык, фреймворк и т.п). Так же, как его выше предложили расширить на тип задачи. Достаточно хорошо известно, что в языках с сильной системой типов необходимость юнит тестов несколько ниже, так как часть гарантий дает компилятор. То есть, часть функций тестов просто перекладывается на описание типов в коде. Ну и напрашивается вывод — разве эффективность TDD от этого же не должна зависеть?
Это отличное замечание! Если честно, я (интуитивно) предполагал, что в языках с сильной и статической типизацией TDD принесет улучшения производительности за счет автоматической генерации кода на основе тестов (как минимум определения классов и методов)

Это отдельная грань проблемы, которую нужно исследовать.
Ну да. При этом я не хочу сказать, что она станет ниже. Но что она станет другой — это почти наверняка. Фактически, мы описали какие-то типы, скомпилировали — это де-факто нам проверяет, что наши типы будут работать правильно (до какой-то степени). Т.е. это тест — но как бы уже и не тест.
НЛО прилетело и опубликовало эту надпись здесь
Почему бессмысленно тестировать отдельные функции?

некоторые элементы семантики которого я продумываю по ходу дела
-> почему не заранее?
НЛО прилетело и опубликовало эту надпись здесь
TDD хорош, когда тесты можно написать до программы.

Кстати, да. У меня были случаи, когда еще на этапе написания тестов по ТЗ становилось понятно, что в ТЗ ошибка.

Я бы добавил ещё один случай — при написании кода, главное в котором:


  1. Скорость выполнения
  2. Работа с файловой системой/сетью/другим железом

Юнит тесты — не особо подходящий инструмент.
Делать методы открытыми, а тем более — заводить интерфейсы ТОЛЬКО для юнит тестов — такое решение может привести к кардинальной деградации производительности.
Если тестировать интеграционниками с использованием не моков, а тестового окружения — то такой «не совсем TDD» может оказаться неплох.
Но если добавить в винегрет ещё и исследования (а при написании тайм-критикал кода они всегда есть), то всё-таки лучше писать тесты после кода.

Я вам вполне могу написать TDD для интеграционных тестов, это не проблема. Даже с учётом скоростей. Вы путаете деление юнит-тесты/интеграционные тесты (что на самом деле вопрос про размер сайд-эффектов) и вопрос "код вперёд или тесты вперёд".


Тут вопрос "а знаем ли мы заранее что мы пишем?" или нет.

Вы путаете деление юнит-тесты/интеграционные тесты (что на самом деле вопрос про размер сайд-эффектов) и вопрос «код вперёд или тесты вперёд».

«Тесты вперёд»(test first) и TDD это разные вещи, и TDD про написание конкретно юнит-тестов.

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

>сайд-эффекты обычно проще придумать
Но зачастую сложнее проверить. Скажем, у нас эффект выполнения программы в целом — это создание каких-то файлов, или скажем запись чего-то в базу. Не то чтобы это было невозможно проверять, но трудоемкость таких тестов сопоставима с трудоемкостью написания основного кода — ну и понятно, что производительность это серьезно снижает.

>TDD ровно так же применим для интеграционных тестов
Ну то есть, наверное где-то применим — но может быть ужасно неудобно, если у вас весь код состоит из интеграций. Пробовал я на примере ESB такое делать… мок на моке сидит, и моком погоняет. А что мы протестировали — очень быстро перестаешь понимать.

Интеграционные тесты единичные, но очень сильные. (и медленные). Я к тому, что для хорошо сформулированной задачи TDD для интеграционных тестов может быть даже более разумным, чем для unit. Чёрный ящик в чистом виде, пиши как хочешь, но тесты должны пройти. При этом сами тесты завязаны на сайд-эффекты, то есть моками их не обманешь.


Утрируя: у нас интеграционный тест для замка с удалённым открыванием. Интеграционный тест выглядит как кронштейн для карточки и тиски для замка, плюс проверка "открылось или нет" (посредством замыкания контакта на приёмнике языка замка).


Дальше вы можете использовать любые методы, но с правильным ключом оно должно открыть, а с неправильным — не открыть. А уж монадки там или ассемблер уже не важно.


И такой тест можно реализовать до того, как будет написана даже первая строчка кода для прошивки замка. И он не поменяется даже если вы отрефакторите всё и вся (кроме форм-фактора самого замка).

Не, я в целом согласен, что так тоже бывает. Ну или иными словами, интеграционные тесты сами по себе TDD не противоречат (если интеграция быстрая, например, то какая разница?).
НЛО прилетело и опубликовало эту надпись здесь
Ну да, TDD начинается там, где исследование закончено и уже стало ясно, как будем решать задачу…

Есть ещё один момент. Бывает так, что не совсем понятно, какую задачу решаем. Т.е. есть интуитивное ощущение "сделать лучше", но как именно — не понятно. И пока не напишешь, понятно не станет. Именно так появляется инновационный (в смысле, "новый в своём классе") софт.

Скорее когда знаем какой результат от алгоритма мы хотим получить. Например если мы знаем что MyAlgoritm(2) при правильной работе должен возвращать 7 или что метод HasHumans(Image) должен вернуть True для картинки с людьми. Короче, TDD хорош когда мы знаем какой ответ хотим получить или эффект сделать для определенных данных. Когда у нас есть ясный вход и выход. При этом мы вообще пока можем не знать как нужный результат нам получить.
Э-э-э TDD не про стратегию, TDD про тактику.
Т.е. мы не знаем точный конечный результат, но в процессе декомпозиции на каждом этапе мы точно знаем, какой конкретно результат хотим получить.
Фиксируем в тесте «гипотезу» (необходимый результат).
А дальше пишем код под эту гипотезу.
Либо гипотеза подтверждается, и мы можем получить необходимый результат.
Либо нет, тогда формируем другую гипотезу.
мне кажется отличное описание. Наверное, стоит провести вебинар по TDD — мне кажется многие коллеги, которые делают выводы о TDD особо никогда не пробовали.
получается как *не читал, но осуждаю*
После прочтения исследований, у меня есть только один логичный ответ — эффективность и применимость TDD зависит, прежде всего, от конкретного разработчика

Эффективность и применимость TDD зависит, прежде всего, от предметной области и особенностей решаемой задачи. О чём выше уже сказали.

Это тоже хорошая гипотеза, но я склонен считать, что разработчик является бОльшим фактором. К примеру, на проекте, где тех лид утверждал что TDD не применимо Эиз-за предметной области" — я «на слабо» сделал case study и показал применимость и улучшение качества, но это в тех случаях, когда я был автором кода.

У вас есть какие-нибудь научные обоснования, или это основано на Вашем опыте?

В любом случае — гипотеза принята к рассмотрению, спасибо.

Вся ваша статья вроде бы о том, что научные обоснования — не обоснования. :)


Моё мнение основано на личном опыте и наблюдении за происходящим в проектах, в которых я участвовал/участвую. Это в основном R&D проекты, зачастую без конкретных ТЗ и спецификаций. Несколько проектов переписывались с нуля несколько раз, всё что создавалось на начальных этапах просто выбрасывалось.


Кстати, тот же спор можно развязать о применимости Agile и прочих скрамов.


Я сторонник разумного подхода. Если в конкретном проекте или части проекта на конкретном этапе его разработки видно, что можно применить TDD, значит нужно попробовать его использовать, хуже точно не будет. Если я вижу, что вот этот конкретный модуль/функция точно будет именно таким и я могу сразу специфицировать его логику и входы/выходы, я сразу пишу тесты.

Вся ваша статья вроде бы о том, что научные обоснования — не обоснования. :)


Не совсем, скорее о том, что у нас нет (пока?) убедительных доказательств любого из утверждений:
1) TDD эффективен
2) TDD не эффективен
3) TDD не оказывает влияния на эффективность

И, соответственно, нужны еще исследования.

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

никак не противоречит применимости TDD, т.к. изменение самих тестов является одним из возможных шагов в «цикле» TDD. С учетом того, что фокус Unit Test это метод или даже определенная ветка в методе, наличие Т.З. не должно оказывать существенного влияния.

Нужно ли лучше формализовать понятие TDD?
НЛО прилетело и опубликовало эту надпись здесь
Отличное замечание! У меня тоже есть вопросы к определению этих понятий, и в исследованиях, действительно, эти понятия не были определенны одинаково.
Есть тезис о том, что юнит-тесты помогают в разработке более качественной и гибкой архитектуры компонентов (тех самых юнитов, которые тестируются). Юнит-тесты помогают снижать связанность, повышать переиспользуемость компонентов, улучшать применение SOLID-принципов.

Почему-то этот тезис используется для подкрепления полезности TDD. Но я не вижу логики в таких рассуждениях. Никто мне не мешает сделать изначально всё хорошо, а потом написать тесты. Или сделать криво, иметь проблемы с написанием тестов, поправить интерфейсы и зависимости и успешно написать тесты. Тесты помогают, да. Почему их надо писать перед написанием кода? Не знаю.
Я больше поддерживаю подход такой:
1. Написали какой-то скелет. Архитектура, интерфейсы устаканились.
2. Написали юнит-тесты.
3. После этого желательно держать уровень покрытия тестами не ниже какой-то планки относительно текущего уровня.
4. Потом, для новой функциональности уже можно делать Test-First или Test-Last. Не уверен, что это имеет большое значение. Имеет значение наличие хороших тестов.
При подходе написания скелета, архитектуры, интерфейсов, не делаем ли мы преждевременных предположений? Как быть, если вдруг окажется, что архитектура неэффективна или нереалезуема для конкретного приложения, среды или языка программирования?
Гипотеза о целесообразности написание тестов до кода я думаю формулируется, примерно, так:
При TDD тесты являются формальным описанием (микро)задачи. Таким образом, как только тест становится «passed» мы можем сделать вывод, что (микро) задача выполнена.

Также, при таком подходе исчезает соблазн «подгонять» тесты под код (а то будет как в лабораторных по теории цепей в старом, добром университете — "пофиг что намеряли, пиши что соответствует заданию, а то все поймут что мы цепь хреново сделали")

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

  • модель в коде функциональности;
  • модель в коде тестов;
  • модель в голове разработчика.


Это отличная цитата из статьи, но не противоречит ли Ваше высказывание Вашей же статье?

Даже если мы признаем утверждение
Тесты — это хороший пример дублирования, метода повышения надёжности за счёт реализации нескольких копий критической части системы. Нюанс заключается в том, что в нашем случае через дублирование контролируется не работа результирующей системы, а точность её формальной модели.

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

Более того, пропоненты TDD, как раз считают, что смысл TDD не в тестах, а в понимании и точности модели.

Ваш ход?
не противоречит ли Ваше высказывание Вашей же статье?

А в чём конкретно противоречие?

Я стою на том, что тесты нужны как хороший способ гарантии качества через дублирование, независимо от порядка их написания. В каком порядке писать зависит от привычек и тараканов разработчиков, от предметной области.

не исключает возможности влияния тестов на качество продукта.

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

Более того, пропоненты TDD, как раз считают, что смысл TDD не в тестах, а в понимании и точности модели.

Ошибка выжившего.

Те, кому легче формировать понимание предметной области тестами, используют TDD по факту своей работы. Они бы его использовали, даже если бы такого названия не было.

Тем, кому легче разбираться с предметной областью не через тесты, TDD мешает и они от него отказываются.
Признаю, я был не внимателен и прочитал как «тесты не нужны».

Ошибка выжившего.

Те, кому легче формировать понимание предметной области тестами, используют TDD по факту своей работы. Они бы его использовали, даже если бы такого названия не было.

Тем, кому легче разбираться с предметной областью не через тесты, TDD мешает и они от него отказываются.


Абсолютно согласен, это по сути — моя гипотеза, но иными словами. Ваша формулировка даже более удачная!
НЛО прилетело и опубликовало эту надпись здесь
Уточнить требования во время написания тестов получится быстрее

Не согласен. Почему?

раннее прототипирование очень полезно.

Согласен, но раннее прототипирование — это не написание тестов перед кодом. Прототип может быть любым: кодом, тестами, моделью в специализированном софте, ролевой игрой с живыми участниками.

Своему коллеге вы предлагаете оценить по тестам вашу реализацию моделей.

Или по коду. Не вижу проблемы.

Суждения о валидности этой модели по отношению к изначальным требованиям не могут быть поревьюены по истории коммитов. Потребуется чтение ТЗ.

Не понял про что это утверждение.

В случае TDD можно посмотреть эволюцию формализации требований.

Эфолюцию формализции требований можно посмотреть по чему угодно, что находится под контролем версий.

Эфолюцию формализции требований можно посмотреть по чему угодно, что находится под контролем версий.


Может ли предположить, что тесты выступают как ubiquitous language, понятный всем разработчикам\заинтересованным лицам?
Если под разработчиками понимается вся команда (включая непрограммистов), то определённо нет.

Тесты будут понятны также как и основная логика, поскольку и то и другое — код. Читают код, который уже в голове интерпретируется в тесты или основную логику.

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

Я, например, предпочитаю смотреть Pull Request начиная с тестов — так я могу понять чего пытались достичь, и более внимательно\продуктивно рассмотреть сам предложенный код.

Но я не могу исключить возможности, что этот метод работает не для всех.
НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории