Pull to refresh

Comments 40

Шаг первый: необходимо признать, что есть проблема. Я думаю, что мы сделали это только что.

Не увидел никакой аргументации против TDD до этой строки. Да и после не особо много аргументов, сплошные эмоции и призыв пользоваться неким тулом.
недостатков два.

первый — я перестал писать качественнылай код без тестов. стал полагаться что юниттесты за этим следят.

второй — если код переписывается даже слегка — юниттесты тоже приходится переписывать
я понял аргументацию так: поскольку наблюдаемая практика регулярно расходится с теорией, значит что-то не так в этой теории — может все это не про нашу реальность, а может ограничения заданы не верно. ведь с момента появления первых упоминаний о tdd уже больше десяти лет прошло, а вопросы все те же.
Согласен. Не ходи в эту церковь, ходи в мою церковь. Я постоянно сталкиваюсь с трудностями на этапе написания тестов, от которых обещает избавить tdd.
Я вам отвечу из моего опыта.
1. Проблема не только в TDD, но и в юнит тестах и автоматических regression тестах вообще.
2. UT создают весьма ощутимое усложнение проекта и увеличение времени разработки.
3. От них на самом деле польза небольшая — они ловят довольно мало реальных проблем.
4. Имеется административная проблема — программистов трудно проверить, насколько качественные тесты они написали. Вы не можете проверить, написан тест «для галочки» или «по существу». И, разумеется, взывать к сознательности в среднем бесполезно.
Об этом я как-то написал заметку тут же на хабре.
Выглядит как начало длинной и обстоятельной статьи или даже книги. И вдруг обрывается…
Продолжение в hangout'ах. Их уже пять и не знаю когда они остановятся. Я так понял, что Давид «набросил», чтобы, в том числе, спровоцировать обстоятельное обсуждение накопившихся проблем.
Очень спорно, я считаю. Подход «мокать все и везде» возник тоже не на пустом месте. Лично по моему опыту, поддерживать полное тестовое окружение (с базами данных, очередями и прочими непотребностями) бывает гораздо более болезненно, чем усложненную архитектуру, предполагающую тесты в изолированном окружении.
Мне вот кажется, что от TDD-подохода «утром тесты, вечером стулья код» можно отказаться лишь тогда, когда у проекта есть не очень большое количество разработчиков и одновременно работа проиходит в 1-5 бранчах. А головняк с поддержкой кучи разных несовместимых версий окружения того не стоит.
По словам автора, TDD не существует без моков. Однако это не так, есть dependency injection и другие подходы, чтобы не использовать моки в юнит-тестах. Я допускаю, что в рельсе у автора есть серьезные проблемы с TDD, но это не повод набрасывать на TDD в целом.
Эм, а одно другое отрицает? Через DI втыкаем или реальный класс, или мокнутный. Или реальный с нужным состоянием.
Я к тому, что уж DI vs Mock — это уж совсем странное противопоставление. :-)
Не обязательно инжектить класс. Можно инжектить то, что он возвращает. Например у меня в iOS проекте есть синглетон, который держит токен для работы с API. Я мог мокнуть этот синглетон, но вместо этого я просто вынес получение токена в отдельный метод, и банально наследуюсь от тестируемого класса, переопределяя данный метод. И без всяких моков и инжектов класса получаю возможность менять токен на лету.

Естественно, это конкретика, но странно говорить, что TDD мертв и не обращаться к конкретным примерам. TDD жив!
> и банально наследуюсь от тестируемого класса, переопределяя данный метод.

Зачем же вы тогда моки используете? Или вы их отрицаете?
В данном случае не использую, но иногда мокнуть проще, чем инжектить. Все зависит от ситуации, и от языка. В тех же рубях например сделать мок намного проще, чем в objective-c, rspec позволяет прямо чудеса творить.

--TDD не существует без моков. Однако это не так

без моков заменять WIN32 довольно сложно — тока делать оберки в классе
Зря минусуете мне кажется. Я тоже считаю, что DHH (автор) зациклился на том, что TDD это обязательно сплошные моки, а если мол моков нет, то это не TDD.
Вот, например, ответ Gary Bernhardt'a на это www.destroyallsoftware.com/blog/2014/tdd-straw-men-and-rhetoric

По hangaut'ам сложилось впечателение, что он уперся рогом и даже не слушает ни Кента ни Мартина. Стоит на ствоем и все тут. В итоге они (Кент и Мартин) пошли на уступки, и «согласились», что TDD в некоторых случаех не так хорошо срабатывает как в других. Но это вовсе не значит, что нужно забыть про TDD именно потому, что Дэвид так решил.
Извините, комментарий хотел на ветку выше оставить.
Во первых TDD и юнит тесты — это все таки не одно и тоже. А автор связал тесно эти два понятия. Например идея написания спецификаций для системных тестов к планнингу перед написанием кода — это по большому счету тоже TDD.

У автора проблемы с модульными тестами по моему. Модульные тесты без TDD вообще редко окупаются на проекте на мой взгляд. С TDD они работают отлично. Но у них есть своя область применения. В первых они требуют архитектурной готовности системы, что усложняет дело для унаследованного кода. Во вторых они выгодны для тестирования своей бизнес логики. В реальности в большинстве проектов своей логики не так уж и много. В основном это разные маппинги и слои для работы с внешними сервисами — которые полезной логики практически не содержат и в покрытие их модульными тестами в изолированном окружении кажется не очень выгодным.

Мне кажется именно в эту ситуацию автор и попал. Со стороны же статья выглядит как «ок — у меня не выходит использовать TDD — поэтому я буду проповедовать, что это зло».

TDD вовсе не мертво и работает. Просто нужно его уметь правильно готовить и выбирать область применимости.
Конечно, статью и выступления DHH не стоит отвязывать от контекста, ибо скорее всего он имеет ввиду, что мы:

* занимаемся веб-разработкой
* есть базовая архитектура (например, Rails)
* в случае Rails юнит-тесты могут спокойно лезть в БД без всяких потерь скорости. (да, это уже не юнит-тесты, но в Rails сообществе никто не заморачивается насчет изоляции тестирования. Чем более реалистичная среда — тем лучше).

TDD где-то и работает. Но почему-то мне кажется, что статистически, число успешных проектов сделанных без TDD намного выше, чем кол-во проектов сделанных в соответствии с ним.
Многие программисты, написав тесты, перестают думать. Перестают моделировать поведение программы в голове и ловить баги «аналитически». А ведь предположения, допущенные при написании теста, далеко не всегда отражают реальность — то, как этот класс будет использовать клиент и в каком состоянии будет система.
Зачастую решение состоит из сложного пересечения модулей, где надеятся на юнит-тесты рискованно.
Действительно IoC решает многие проблемы с моками, но вопрос, нужны ли юнит тесты и насколько? Сколько стоит их поддерживать и оправдывают ли они себя? Или всё-же лучше сделать ударёние на приёмочные и интеграционные тесты — которые медленней, гоняются на сервере — но при этом тестируют интеграцию нескольких компонент, практически не изменяются с кодом и могут рассказать какую-то бизнесс историю, будучи написанными читабельно?
В каждом проекте по-своему, призыв прост — перестать из ТДД делать священный грааль — часто юнит тесты увеличивают стоимость содержания кода, не давая ничего взамен.
Ну модульные тесты всегда дают что то взамен. Но вопрос окупаемости у них действительно стоит остро.

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

TDD вообще не так уж часто используют на самом деле. И есть огромное количество людей — которые их боятся применять — потому что не умеют или умеют неправильно. По моему перестать пропагандировать TDD когда нибудь надо будет — но пока еще рано. Попытки его использования позволяют пересмотреть архитектуру, выделить значимую логику, походящую для тестирования и избавиться от незначимой (разные бесполезные маппинги и слои). Пусть это побудет еще немного священным граалем. В моей практике TDD может быть невероятно выгодным. И стоимоть поддержки тестов может окупиться с лихвой. И скорость разработки с TDD через модульные тесты может быть выше чем вообще без тестов — как это парадоксально не звучит.
Многие программисты, написав тесты, перестают думать. Перестают моделировать поведение программы в голове и ловить баги «аналитически».

У меня часто бывает наоборот: когда код разрабатывается через тесты, то получается заметно проще придумывать альтернативные случаи, которые могут испортить его поведение. Каждый такой случай превращается в новый тест. Чаще всего это получается, если начинать создавать систему с самых простых тестов и наращивать поведение шаг за шагом. Тесты ощупывают код, словно пальцы, и замечают малейшие шероховатости в коде, что и позволяет вовремя реагировать на них.

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

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

А не надо переставать думать. Причем тут тесты?
Комментатор выше всё написал. Основная проблема тестов на «моках», что они позволяют программисту полностью отстраниться от проблемы. «Если тест на моке прошёл, значит это не проблема моего кода, разбирайтесь дальше сами, что у вас там база/контроллер/внешний сервер чудит».
Тесты как раз и отвечают за код своего приложения. Если чудит внешнее API хостера, то это проблемы хостера. Программист разве что в саппорт может отписать. Разве нет?
Вот тут как раз и зарыт корень проблем. Если тесты проходят, значит чудить может только чужое приложение, радостно отвечает программист и его проблема больше не волнует.

А это может быть нелепый race condition с записью в базу. Или дедлок на запросах с двумя копиями одного приложения. Или логика приложения и реализация его mock'ов в некоторых экстремальных случаях даёт ошибку, под которую написан код (и которой нет в реальных реализациях в продакшене, так что ошибка возникает уже в продакшене). Или логика двух модулей содержит ошибки.

Но всё это — «чудит внешнее API, проблемы хостера».

В этом и есть проблема «чистого TDD». Оно даёт индульгеницю программисту отмахиваться от всего, что не совпадает с его мироощущением, изложенном в mock'ах.
Я предполагал все-таки, что код и моки написаны корректно, потому что разговор идет о самом TDD. Ошибки случаются всегда, и четкой корреляции с TDD тут нет.
Оно даёт индульгеницю программисту отмахиваться от всего, что не совпадает с его мироощущением, изложенном в mock'ах.

Скорее оно позволяет быстрее локализовать проблему в большинстве случаев. Хотя точка зрения DHH мне импонирует, часто написание юнит тестов (а особенно стремление к 100% code coverage) себя не оправдывает.
Моки написаны согласно пониманию программиста. А оно не всегда совпадает с реальностью. Согласно моему опыту, проблемы интеграции, по сути неверного понимания протокола, встречаются на порядок чаще чем обычные ошибки в коде. По крайней мере это верно для опытных программистов, с которыми я имел счастье по большей части работать.

+1. Моки могут легко и незаметно устареть. Пускай даже на момент создания они полностью соответствуют поведению реального кода. Но если этот код не стоит на месте, а развивается, то нередко бывает, что через несколько месяцев его поведение уже начинает в каких-то деталях отличаться. Если эти изменения забыли добавить в моки, то и код, который на них полагается, уже нельзя считать таким же надёжным, как ранее.


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

Простите, а как же интеграционные тесты? Я понимаю, что и они всего не покрывают, но увеличивают надежность. Мне кажется нельзя гворить о работоспособности всего приложения, исходя только из того, что работают модульные тесты(у меня лично были ситуации, когда код проходит все юнит-тесты на ура и с хорошим покрытием, а в конфигурации понаписано такое, что оно просто никак не может работать). И мне кажется, создавая код, которые работает с «чужим» приложением (как пример — внешнее API), нельзя не писать интеграционных тестов.
Так речь про это и идёт. Если у нас 'TDD', то когда у «программиста всё работает» (читай, юнит-тесты проходят), то на свалившихся интеграционных тестах программист занимает позицию «это там у вас что-то не так».

Тут надо понимать, что есть «светлая сторона TDD» — то, как задумывалось, и «тёмная сторона TDD» — то, как его используют. Проблема в том, что TDD позволяет очень ярко провести линию раздела и даёт некую психологическую установку, с которой можно бороться, но которая есть. Выше тред и комментарии как раз про последствия этой установки.
такая установка и без tdd встречается довольно часто.
Но TDD провоцирует и даёт основание.
Sorry, никогда не встречался с таким отношением. По моему опыту: либо проект летит вообще без тестов(ну исторически так сложилось), либо тесты пиуштся более-менее адекватно и по делу.
Тесты пишутся. А я говорю про ситуацию, которая происходит, когда «тесты показывают, что всё ок», а «в продакшене оно как-то странно себя ведёт».
У нас именно так и было.
Юнит тесты не помогают с проблемами интеграции и мультисрединга.

Я об этом целый пост написал.
Как-то не увидел аргументов в статье и обстоятельного освещения вопроса, анализа. Есть плюсы, есть и минусы. А так больше похоже на очерк, эссе «Как мне не понравилось ТДД».
Вставлю и я свои 5 коепеек:
Подход «тесты вперед» приводит к чрезмерно сложной структуре посреднических объектов и окольных путей

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

В итоге мы приходим к созданию монструозной архитектуры

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

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

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

Мой пост на эту тему:
Опыт работы с TDD и размышления о том, как надо тестировать код.
Помните старую шутку Задача: прострелить себе ногу?

Я вот подумал, как она будет реализовываться с TDD:

Вы делаете гипсовую модель ноги. В лаборатории простреливаете ее из пистолета. Убеждаетесь, что диаметр и положение входного и выходного отверстий вас устраивает. Затем вы долго и упорно создаете машину, делающую дырки подходящего диаметра при помощи соответствующей силы удара. Испытываете ее подставляя под нее ногу. Убеждаетесь, что характер и степень повреждений вас устраивает. Наконец вы стреляете себе в ногу.
Sign up to leave a comment.

Articles