Есть полно исследований, демонстрирующих эффективность TDD
Действительно. Если зайти на Google Scholar, забить ключевые слова «TDD» и «Эффективность» — будет много научных статей, но так ли все просто? Хоть я сам и являюсь большим фанатом TDD, но я так же считаю себя скептиком, и решил проверить, доказано ли научно, что TDD так крут.
Test-driven development (TDD) с нами уже долгое время, и все это время он был предметом горячих споров. Суть TDD в том, что начинать разработку надо с написания падающего Unit test, после чего пишется код, который сделает этот тест «зеленым», и так по циклу. Для тех, кто не в курсе, можно посмотреть руководство по TDD в IDE Idea.
Часть разработчиков уверены, что TDD улучшает эффективность и качество не менее чем до 146%. Часть разработчиков точно знает, что TDD — это полный бред, и писать тесты до написания кода — это как кататься лыжами по асфальту. Компромисса между этими группами разработчиков ждать не стоит.
Но вернемся к науке. По счастливой случайности, так совпало, что я учусь в магистратуре Wrexham Glyndwr University по программе Computer Science with Big Data Analytics (кстати — всем рекомендую, и распишу про нее отдельно) и прямо сейчас прохожу модуль «Critical Research», где учат читать научные статьи и анализировать их достоверность.
Какие наши доказательства?
Исследований на тему эффективности и полезности TDD, действительно, много. Однако, если почитать сами исследования, можно заметить, что они приходят к противоречивым выводам.
К примеру, в докладе “An Experimental Evaluation of the Effectiveness and Efficiency of the Test Driven Development”, опубликованном в 2007 был сделан вывод, что TDD улучшает продуктивность разработчиков, при этом не оказывая влияния на качество [1].
В другом докладе, в котором были рассмотрены выводы нескольких других исследований и проведено собственное исследование, “Evaluating the Effectiveness of Test Driven Development: Advantages and Pitfalls”, 2017 пришли к выводу, что TDD может улучшить качество продукта, но приводит с снижению продуктивности. Дополнительно, было замечено, что большинство разработчиков находят TDD неестественным [2].
По результатам еще одного исследования, “A Replicated Experiment on the Effectiveness of Test-First Development”, 2013 исследователи не смогли обнаружить убедительного и значимого влияния Test-First или Test-Last подходов на продуктивность разработчика или качество продукта [3].
Чтобы забить гвоздь в гроб «доказанности» эффективности и полезности TDD, можно почитать обзорный отчет “Overview of the Test Driven Development Research Projects and Experiments”, 2012 [4], в котором, на основе большого количества доступных исследований пытались придти к выводу о доказанности эффективности и положительном эффекте TDD. Авторы заключили, что не существует убедительных доказательств о положительном эффекте TDD. Различия в результатах предыдущих исследований авторы обосновали Confirmation bias — исследования, которые показали эффективность TDD изначально исходили из гипотезы, что TDD — эффективен.
Так почему же люди (включая меня), считают TDD отличной практикой?После прочтения исследований, у меня есть только один логичный ответ — эффективность и применимость TDD зависит, прежде всего, от конкретного разработчика. Исследования эффективности TDD, видимо, недостаточно учли, что разработчики — не роботы, а TDD — не программа. Для части разработчиков, TDD принесет хорошие результаты, для части — не будет значимой разницы между TDD и простым написанием Unit tests, для части — TDD приведет к падению качества, продуктивности и мотивации.
Программирование — это социальная активность. Проблема с социальным взаимодействиями — это то, что это не точная наука. Осталось придумать как провести более-менее научный эксперимент чтобы подтвердить\опровергнуть эту гипотезу. Для начала предлагаю опрос.
Если вдруг вы захотите поделиться этой публикацией с англоязычными коллегами — есть чуть более формальная английская версия: medium.com/@alexspush/is-tdd-effective-2474c0471ac4
Список источников
[1] A. Gupta, P. Jalote “An Experimental Evaluation of the Effectiveness and Efficiency of the Test Driven Development” presented at First International Symposium on Empirical Software Engineering and Measurement, Madrid, Spain, 2007
[2] Z. Khanam, M. Ahsan, “Evaluating the Effectiveness of Test Driven Development: Advantages and Pitfalls” International Journal of Applied Engineering Research vol. 12, no 18, p. 7705, 2017. Available: www.ripublication.com/ijaer17/ijaerv12n18_81.pdf. [Accessed May 13, 2020].
[3] D. Fucci, B. Turhan, “A Replicated Experiment on the Effectiveness of Test-first Development” presented at ACM / IEEE International Symposium on Empirical Software Engineering and Measurement, Baltimore, MD, USA, 2013
[4] A. Bulajic, S. Sambasivam and R. Stojic “Overview of the Test Driven Development Research Projects and Experiments” presented at Proceedings of Informing Science & IT Education Conference (InSITE), 2012. Available: pdfs.semanticscholar.org/3278/971bb53a25822171df127d47a57243dd3bbd.pdf. [Accessed May 13, 2020].
amarao
TDD хорош, когда тесты можно написать до программы. Очень часто программирование exploratory, т.е. "что получится никто не знает". В этой ситуации игра в TDD это глупость.
А бывает так, что есть совершенно точное ТЗ к которому можно написать тест до кода, и даже можно написать проходящий тест мок (на одном наборе данных), а потом можно писать код.
Это совершенно разные задачи — одно (exploratory) это элемент НИОКР, а второе — просто кодинг. Вот для задач кодинга TDD кратно повышает качество кода. Для НИОКР качество кода вторично по сравнению с нахождением решения, так что TDD лишь мешает искать решение.
Вот и всё.
vdem
Вот и я сначала пишу какую-то небольшую часть кода, прикидываю, что получается, и когда картина более-менее ясна, дальше уже продолжаю кодить одновременно с написанием тестов. Здесь тесты очень помогают не сломать что-то очередными изменениями.
amarao
Это вы описываете как вы решаете простые задачи простыми методами. Бывает так, что результат exploratory programming не ясен до самого конца, потому что есть хаотические факторы (нагрузка, конкурентность, маштаб, невидимые обстоятельства-состояния). Бывает так, что даже задача не ясна "я хочу примерно вот так… или лучше вот так". Отказываться от такого — лишать продукт права на развитие. (Да, мы все умеем писать TDD для CRUD. Слабо написать тесты для ещё ненаписанной программы, которая делает удобно при редактировании текста?)
Повторю тезис: TDD хорош для задач кодинга, когда не нужно исследовать.
senpay Автор
Формализуйте, пожалуйста, понятие exploratory programming? Можно конкретный пример? Я тогда бы учел это в исследовании, у меня, по ходу дела, отличная идея диссертации намечается.
amarao
Прямо из бэклога:
Нужно переносить сервера (инстанса приложения) между разными кластерами в разных стойках (т.е. с разными Top-of-Rack свитчами) с минимальным даунтаймом.
Это, кстати, почти контр-пример для моего тезиса, потому что тест для определения даунтайма я могу с лёгкостью написать даже не зная как я буду реализовывать её.
… Давайте более exploratory.
Я хочу в терминале возможность прыгать между вызовами команд. Т.е. отдельный комплект хоткеев, который позволяет переходить между строчками, где начинается вывод от новой команды с шелла.
Ну какие тут TDD?
senpay Автор
Немного не понял задачу с терминалом. В случае, если известен формат строчек и набор хоткеев, написать тест не составит труда.
Можете пояснить чуть более подробно?
amarao
Формат строчек не известен.
Вот у меня шелл на удалённый сервер:
$ 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. Пойти туда, не знаю куда, принеси мне Фичу.
senpay Автор
Так понятнее, спасибо. Действительно в данном примере я не вижу даже целесообразности TDD.
chapuza
А я в данном примере вообще не вижу целесообразности тестов. Потому что простые случаи легко покрыть прямо запусками из терминала, а для внезапных граблей никакой мегамозг тест не напишет, — до того, как на грабли эти наткнется.
Если можно менять
PS1
— задача вырожденно-тривиальная. Если нельзя — в общем случае не решаемая, потому что удаленный шелл не нанимался помнить историю вывода и туннели через 2+ssh
все выкрутасы с радостью похерят.Если я хочу это в своем уютненьком терминале на лаптопе — тут вообще делать нечего,
Enter
перехватить и ага.Но правильное решение такой задачи внезапно тривиально: хоткей, который маппится на «поиск назад строки, содержащий текущий промпт». Будут ложные срабатывания? — Да, возможно. Критично? — Вот вообще нет. Хоткеями пользуются люди, нажмут еще раз, если вдруг что.
Писать на такую задачу тесты — это вообще себя не уважать.
senpay Автор
Я думаю, если речь идет о платном приложении, нестабильность горячих клавиш может легко стать причиной неуспеха.
Я вот очень сильно переживаю, если у меня хоткеи не работают надежно.
И не тестировать такой функционал — это не уважать пользователя, а писать ненадежный софт — не уважать себя. Разве нет?
Есть отличная глава "QA should find nothing" из книги Clean Coder.
chapuza
Больше всего в современном состоянии CS меня напрягает бесконечное обилие никому не нужных методологических книг на фоне абсолютного отсутствия руководств по таким насущным вопросам, как умение понимать поставленную задачу.
Платное приложение? С гарантией работы горячих клавиш? Ну, допустим. Это совершенно не та задача, которую мы тут обсуждаем, но допустим.
Начните с написания терминального клиента. При установке соединения с удаленным хостом, загрузите туда свой код, который перехватит запуск из шелла и будет по определенному вами бинарному протоколу в отдельном канале присылать вам метаинформацию. Обмажьте это тестами (а если ваша команда упоролась по хайпу — то и типами) со всех сторон.
Настройте стенд с матрицей всех шеллов всех версий, популярность которых выше 0.1%. Прогоните все тесты там.
Можно запускаться.
Есть отличный способ изюежать необходимости читать много водянистой беллетристики: думать своей головой.
JediPhilosopher
Ну вот у меня например задача была — закодить систему генерации городской застройки. Т.е. даешь ей полигон участка, она сама раскидывает там домики и дороги, с учетом градостроительных норм и всяких принципов хипстоурбанизма.
Ну и я вот месяц сидел и фигачил туда-сюда всякие алгоритмы расстановки дорог и домиков.
Тут во-первых непонятен результат (ну то есть хочется получить красивую застройку, но непонятно даже как именно она должна выглядеть, это плохо формализуется, а самих норм недостаточно). Во-вторых надо очень быстро и много чего пробовать и менять — от тестов тут толку особо нет, так как их придется постоянно переписывать, по мере смены концепций и алгоритмов.
Вот когда уже получен хороший результат, и теперь надо на его основе сделать устойчивый продукт — там уже можно написать тесты, а потом начать разгребать всю ту кучу говнокода, которая родилась в процессе исследований и прототипирования.
senpay Автор
Ваш аргумент отлично работает против ATDD (Acceptance Test-driven development), но не TDD.
В классическом TDD тест — это Unit-test, а тестируемый объект это метод, или даже отдельная ветка исполнения в методе. Готов поспорить, что на этом уровне в любом приложении будут детерминированные результаты.
sshikov
Детерминированные — да. Но это не всегда значит, что будет просто или быстро написать для них assert. Ну скажем, в моем случае, программа это spark (примерно тоже самое будет верно и для пандас, я думаю). Уже на самом верхнем уровне (собственно, там и кода может быть совсем мало) мы сталкиваемся с тем, что тестируемые объекты — это что-то, совершенно непригодное для юнит тестирования. Т.е. это датафреймы или датасеты, и чтобы они работали, нужно запустить собственно некий здоровый фреймворк.
И тесты сразу становятся интеграционными, и перестают работать приемлемо быстро. Это полностью лишает смысла применять такие тесты в методологии типа TDD, когда быстрый ответ важен.
senpay Автор
Согласен, для таких задач Unit-Test слабо применимы. Хотя, есть примеры, когда в таких задача прекрасно работал Test-First подход с BDD фреймворками, например — www.youtube.com/watch?v=bny86gxbUcg
DistortNeo
Отличие TDD от code-first заключается в том, что TDD принуждает к максимальному покрытию кода тестами и к написанию качественного кода. При использовании code-first велик соблазн оставить кусок кода непротестированным, ведь и так всё работает. При TDD такого не будет.
Но аргументы, написанные выше, не столько против TDD, сколько против тестов вообще в определённых случаях.
Дело в том, что основной смысл в тестах — это фиксация поведения программы при определённых сценариях, чтобы не допустить регрессий при доработке программы. Также тесты играют роль документации. При это тесты не гарантируют корректность работы программы. Тесты, как и код, могут содержать ошибки. Тесты могут быть неполными даже при TDD.
Нужно ли писать тесты, если задача поисковая? Моё мнение — нет, потому что поведение программы постоянно меняется, а тесты, наоборот, его фиксируют. То есть придётся делать двойную работу: переписывать и код, и тесты. А если это обработка изображений, так затраты на тесты там вообще на порядки выше, чем на написание кода. Работоспособность алгоритмов намного проще проверить глазами.
Ну то есть тут уже всё становится индивидуально. Есть люди, кому проще писать код сразу с тестами — ок, пусть пишут. А если люди, кому это в тягость — пусть не пишут.
senpay Автор
Вот и мой основной тезис в том, что все очень индивидуально, и то, что работает для одного разработчика, может прекрасно не работать для другого
mad_nazgul
Прошу прощения, но ИМХО как раз тесты могут быть инструментом исследования!
Т.е. мы хотим получить какой-то результат (как его получить мы не знаем).
В тестах мы фиксируем результат.
А потом последовательными итерациями к нему приближаемся.
Или мы «тестируем гипотезу».
Опять же в тестах мы формулируем гипотезу и смотрим, что получается/не получается.
senpay Автор
Мне нравится такой подход — "тест как гипотеза".
Ведь, в противном случае, как мы поймем, что мы наиследовали своим кодом? Разве что читать логи, что не всегда реально (если их тыщи)
sshikov
Я думаю, что тезис «эффективность зависит от… разработчика» нужно просто расширить, например на инструмент (язык, фреймворк и т.п). Так же, как его выше предложили расширить на тип задачи. Достаточно хорошо известно, что в языках с сильной системой типов необходимость юнит тестов несколько ниже, так как часть гарантий дает компилятор. То есть, часть функций тестов просто перекладывается на описание типов в коде. Ну и напрашивается вывод — разве эффективность TDD от этого же не должна зависеть?
senpay Автор
Это отличное замечание! Если честно, я (интуитивно) предполагал, что в языках с сильной и статической типизацией TDD принесет улучшения производительности за счет автоматической генерации кода на основе тестов (как минимум определения классов и методов)
Это отдельная грань проблемы, которую нужно исследовать.
sshikov
Ну да. При этом я не хочу сказать, что она станет ниже. Но что она станет другой — это почти наверняка. Фактически, мы описали какие-то типы, скомпилировали — это де-факто нам проверяет, что наши типы будут работать правильно (до какой-то степени). Т.е. это тест — но как бы уже и не тест.
0xd34df00d
Мой пример: тайпчекер и транспилятор для игрушечного proof-of-concept-языка, грамматику и некоторые элементы семантики которого я продумываю по ходу дела. Единственные тесты, которые у меня есть — что такое-то выражение парсится корректно (когда писал парсер), что такое-то выражение принимается или отвергается тайпчекером (когда писал тайпчекер), что такое-то выражение после транспиляции на таком-то входе даёт такой-то результат.
Тестировать отдельные функции тут как-то бессмысленно, есть только интеграционные тесты.
senpay Автор
Почему бессмысленно тестировать отдельные функции?
-> почему не заранее?0xd34df00d
А зачем? Что именно здесь (и в соседних файлах) тестировать по отдельности и, опять же, зачем?
Заранее всё продумать тяжело. Какие-то идеи возникают по ходу дела, например. Вот, недавно оказалось, что паттерн-матчинг таки неплохо добавить и проверить.
lair
Кстати, да. У меня были случаи, когда еще на этапе написания тестов по ТЗ становилось понятно, что в ТЗ ошибка.
HackerDelphi
Я бы добавил ещё один случай — при написании кода, главное в котором:
Юнит тесты — не особо подходящий инструмент.
Делать методы открытыми, а тем более — заводить интерфейсы ТОЛЬКО для юнит тестов — такое решение может привести к кардинальной деградации производительности.
Если тестировать интеграционниками с использованием не моков, а тестового окружения — то такой «не совсем TDD» может оказаться неплох.
Но если добавить в винегрет ещё и исследования (а при написании тайм-критикал кода они всегда есть), то всё-таки лучше писать тесты после кода.
amarao
Я вам вполне могу написать TDD для интеграционных тестов, это не проблема. Даже с учётом скоростей. Вы путаете деление юнит-тесты/интеграционные тесты (что на самом деле вопрос про размер сайд-эффектов) и вопрос "код вперёд или тесты вперёд".
Тут вопрос "а знаем ли мы заранее что мы пишем?" или нет.
EvgeniiR
«Тесты вперёд»(test first) и TDD это разные вещи, и TDD про написание конкретно юнит-тестов.
amarao
Ой ли? TDD ровно так же применим для интеграционных тестов, как и для юнит-тестов. Я бы сказал, что он даже более применим, потому что сайд-эффекты обычно проще придумать (до написания кода), чем потрошки интерфейсов.
sshikov
>сайд-эффекты обычно проще придумать
Но зачастую сложнее проверить. Скажем, у нас эффект выполнения программы в целом — это создание каких-то файлов, или скажем запись чего-то в базу. Не то чтобы это было невозможно проверять, но трудоемкость таких тестов сопоставима с трудоемкостью написания основного кода — ну и понятно, что производительность это серьезно снижает.
>TDD ровно так же применим для интеграционных тестов
Ну то есть, наверное где-то применим — но может быть ужасно неудобно, если у вас весь код состоит из интеграций. Пробовал я на примере ESB такое делать… мок на моке сидит, и моком погоняет. А что мы протестировали — очень быстро перестаешь понимать.
amarao
Интеграционные тесты единичные, но очень сильные. (и медленные). Я к тому, что для хорошо сформулированной задачи TDD для интеграционных тестов может быть даже более разумным, чем для unit. Чёрный ящик в чистом виде, пиши как хочешь, но тесты должны пройти. При этом сами тесты завязаны на сайд-эффекты, то есть моками их не обманешь.
Утрируя: у нас интеграционный тест для замка с удалённым открыванием. Интеграционный тест выглядит как кронштейн для карточки и тиски для замка, плюс проверка "открылось или нет" (посредством замыкания контакта на приёмнике языка замка).
Дальше вы можете использовать любые методы, но с правильным ключом оно должно открыть, а с неправильным — не открыть. А уж монадки там или ассемблер уже не важно.
И такой тест можно реализовать до того, как будет написана даже первая строчка кода для прошивки замка. И он не поменяется даже если вы отрефакторите всё и вся (кроме форм-фактора самого замка).
sshikov
Не, я в целом согласен, что так тоже бывает. Ну или иными словами, интеграционные тесты сами по себе TDD не противоречат (если интеграция быстрая, например, то какая разница?).
0xd34df00d
Заворачиваетесь в свободную монаду и интерпретируете.
Pieceofduke
Ну да, TDD начинается там, где исследование закончено и уже стало ясно, как будем решать задачу…
amarao
Есть ещё один момент. Бывает так, что не совсем понятно, какую задачу решаем. Т.е. есть интуитивное ощущение "сделать лучше", но как именно — не понятно. И пока не напишешь, понятно не станет. Именно так появляется инновационный (в смысле, "новый в своём классе") софт.