Test-driven development (TDD) — практика, известная уже довольно давно. Разработка через короткие циклы «прежде всего пишем юнит-тест, затем код, потом проводим рефакторинг, повторяем» в ряде компаний принята в качестве стандарта. Но обязательно ли команда, достигшая хорошей степени зрелости процесса разработки, должна принимать TDD? Как и для большинства других практик Extreme Programming, споры по поводу TDD до сих пор не стихают. Оправдываются ли первоначальные затраты на обучение и внедрение TDD? Даёт ли TDD ощутимый выигрыш? Можно ли этот выигрыш измерить? Нет ли случаев, когда TDD проекту вредит? А есть ли ситуации, когда без TDD решить задачу просто невозможно?
Об этом мы поговорили с разработчиками-экспертами Андреем Солнцевым asolntsev (разработчик из таллинской компании Codeborne, который практикует Extreme Programming и придерживается TDD) и Тагиром Валеевым lany (разработчик в JetBrains, также разрабатывает опенсорсную библиотеку StreamEx и анализатор байткода Java HuntBugs; убежден, что TDD — бесполезная практика). Интересно? Добро пожаловать под кат!
JUG.ru Group:
– Добрый день, Андрей, добрый день, Тагир! Таллин, Москва и Новосибирск на связи в Skype. И чтобы читателям стала ясна ваша позиция, первый вопрос короткий: работаете ли вы по TDD?
А. Солнцев:
– Да, мы работаем по test-driven development каждый день. То есть мы пишем тест и код. И я считаю, что это очень полезная и правильная практика. В идеале практически все должны так работать, чтобы быть эффективными.
Т. Валеев:
– Я не работаю по test-driven development в том смысле, в котором работает Андрей. Я для новой функциональности совершенно точно сперва пишу код, и потом только тесты к нему. В случае с баг-фиксами – 50/50: если у меня уже есть готовый код и мне пришел report, что он где-то падает, я могу написать тест, воспроизводящий эту проблему, и потом исправить, либо могу сперва исправить, а потом написать тест. Но юнит-тестами у меня код все равно оказывается покрыт. Весь новый код, который я пишу, я обязательно покрываю юнит-тестами.
JUG.ru Group:
– Test-driven development известен очень давно, с начала двухтысячных годов. Однако в массе своей разработчики не работают по TDD, по моему ощущению. Почему это происходит? Я задавал этот вопрос Николаю Алименкову xpinjection. Он назвал две причины. Первая – просто не умеют. Их никто не учил, как это делать правильно. И вторая – ошибочно себя считают крутыми архитекторами: «Сейчас я быстренько тут нагенерю из паттернов некую структуру, и она сразу будет работать». Ваше мнение каково? Почему TDD не используется массово?
А. Солнцев:
– Я согласен с Николаем. Действительно, так и есть. Не умеют. И тут беда в том, что мало прочитать какое-нибудь руководство или книжку, чтобы уметь: так не работает. Ты все равно, когда начинаешь пробовать, что-то делаешь не так. Я, например, когда впервые прочитал про юнит-тесты и в своем рабочем проекте начал их применять, потом постфактум понял, что я делаю это абсолютно, в корне неправильно. И все, что я сделал за год, можно выкинуть.
JUG.ru Group:
– Тагир, а почему вы не работаете по TDD?
Т. Валеев:
– Я считаю просто, что это не нужно. То есть я понимаю исключительную важность юнит-тестов, важность хорошего покрытия тестами, но писать тесты вперед кода — это лишняя трата времени и ресурсов. Результат от этого не становится лучше, а времени на это будет затрачено, скорее всего, больше.
Quick fix vs. Code completion
А. Солнцев:
– А почему ты думаешь, что это так? Если все равно ты напишешь ровно столько же тестов, то почему, когда их пишешь первыми, то времени уходит больше?
Т. Валеев:
– Как минимум, у тебя не будет code completion в IDE. Если ты пишешь тест, к которому нет кода, то ты не можешь его писать достаточно эффективно.
А. Солнцев:
– Ну, это легко решается. Ты же не пишешь тест от и до полностью. Ты пишешь сначала обращение к методу, которого нет. Да, тут у тебя нет code completion, и это хорошо, потому что тогда ты продумаешь, как метод будет называться. Написал обращение к методу — можешь нажать Alt+Enter, в IDE сгенерируется пустой метод. Все, дальше у тебя появится code completion.
JUG.ru Group:
– Более того, ты продумаешь не просто как он будет называться, а какие параметры у него будут и как его вызвать наиболее удобным образом.
Т. Валеев:
– Я не согласен, что для того, чтобы генерировать код, использование quick-fix-ов проще, чем использование code completion. Но, возможно, это вкусовщина.
А. Солнцев:
– Не проще, а примерно одинаково и так, и так. Alt + Enter нажать и в том, и в том случае.
Т. Валеев:
– Нет, я думаю, что использование quick-fix-ов сложнее. Я потрачу больше времени. И нажать надо не только Alt + Enter. Например, я вызываю в тесте такой-то метод и, скажем, считаю, что с таким-то строковым и с таким-то числовым параметром должен выдавать такое-то значение. Я написал ассерт. Метода у меня еще нет и класса тоже нет. Таким образом, я должен сперва сказать: «Создай класс, которого еще нет». Выбрать для него пакет. То есть я не просто нажал Alt + Enter, я еще в диалоге заполнил пакет, область видимости класса…
А. Солнцев:
– Так все эти вещи ты все равно будешь делать так или иначе, без разницы.
Т. Валеев:
– Но я их буду писать в текстовом редакторе, а не в диалоге. Хорошо, с классом разобрались, вот имя метода. В этом случае я нажимаю Alt + Enter и получаю диалог, в котором мне предлагают заполнить название параметров. Так как у меня тест содержит строку и число, IDE у меня не отгадает, как у меня эти параметры должны называться. То есть я должен буду в диалоге вводить их названия. Возможно, она также неправильно угадает их типы, особенно, если я предполагаю, что у меня там сложный тип какой-нибудь. Мне гораздо проще вручную набрать это в редакторе.
А. Солнцев:
– Я повторюсь еще раз, это одинаково по времени. То есть ты все равно должен будешь в конечном итоге, если у тебя имя параметра состоит из десяти символов, нажать десять клавиш. В любом случае ТDD или не TDD, это будет совершенно одинаково.
Т. Валеев:
– Тут есть два варианта. Первый: я в диалоге оставлю все как есть, и потом в текстовом редакторе буду просто править то, что мне нагенерировали (или в диалоге поправлю то, что мне нагенерировали). Второй: я просто введу в пустом текстовом файле сразу же то, что я хочу, не исправляя.
А. Солнцев:
– Абсолютно без разницы.
Т. Валеев:
– На мой взгляд, разница есть.
Удобство использования vs. эффективность реализации
JUG.ru Group:
– Но, коллеги, все-таки за TDD говорит еще вот какой факт. Если сначала ты пытаешься написать тест, тебе сначала придется выполнить setup. Тест – это ведь не только вызов метода, это же еще и setup. При помощи конструктора или фабрики ты собираешься создавать тестируемый объект? В конструктор или в свойства ты будешь передавать какие-то параметры? Где ты эти параметры возьмёшь? И этот момент очень ценный, потому что если ты сразу начинаешь думать с позиции того, как это использовать, то у тебя получается более красивое API.
А. Солнцев:
– Абсолютно верно, согласен.
Т. Валеев:
– Это очень хорошая тема. И здесь как раз можно поспорить, потому что любое API стоит рассматривать как с точки зрения удобства использования, так и с точки зрения удобства реализации. И более того — эффективности и вообще возможности реализации, потому что может оказаться, что API, удобное с точки зрения использования, неудобно с точки зрения реализации потому, что ему просто не хватает данных. Я, например, пишу IDE. Это то, чем я реально сейчас занимаюсь. Мне, например, нужен новый метод, который мне найдет класс. Я хочу, чтобы по имени “java.util.Collection” я нашел класс и узнал, какие там методы. Я, естественно, думаю: «Что мне нужно? Мне нужно имя с типом String». Хорошо, я пишу, допустим, метод findClass(String name), передаю ему строку “java.util.Collection” и проверяю, что он что-то должен найти. Хороший, удобный тест? Куда проще. Но когда вы начнете реализовывать, вы поймете, что “java.util.Collection” – это непонятно что, потому что у вас, например, в разных модулях или в разных проектах может быть подключен разный JDK и это имя может соответствовать разным классам, в которых разное количество методов. Когда вы будете это реализовывать, вы об этом не сможете не подумать, потому что вы сразу поймете, что есть проблема. Нужна привязка к проекту или к какому-нибудь resolve scope. Соответственно, в вашем удобном способе использования просто не хватает данных, чтобы реализовать результат. Так что написание теста не позволит сделать хороший API. В данном случае мы написали тест, нам было удобно им пользоваться, но API получился плохой.
А. Солнцев:
– Ну и что? Не хватает данных, так и есть, но я не вижу тут никакой проблемы. Ты начинаешь писать с теста. Написал тест так, как максимально удобно было бы его использовать. По мере реализации ты видишь, что не хватает каких-то данных, ты возвращаешься на шаг назад и понимаешь: «Ок, так невозможно, нужны новые данные». И ты снова дополняешь тест и думаешь, как наиболее удобным способом передать эти данные в этот метод через конструктор, через injection service, как угодно. Соответственно, дополняешь ты тест, чтобы передать эти данные, и начинаешь дальше реализовывать. Я не вижу проблемы. Все так и есть. Evolutionary design.
Т. Валеев:
– В том-то и дело, что ты не видишь проблемы, а я не вижу, зачем писать тест, чтобы потом увидеть, что этот тест бесполезен, что так не заработает? Зачем делать эти шаги вперед, шаги назад, когда можно сразу же написать реализацию? И она будет сделана максимально удобным способом и не придется ни разу переписывать тест. Сразу напишешь реализацию, потом тест. Ни одного шага назад ты не делаешь. Это эффективно.
А. Солнцев:
– Эффективно для кого? Мы возвращаемся к самым основам: зачем вообще нужен TDD? Он позволяет заранее продумать, как наиболее удобным способом использовать API. Ты сейчас упомянул в твоем примере, что тебе нужно передать, во-первых, имя класса, плюс нужно каким-то образом передать еще дополнительные параметры, скажем, проект, версию Java – какой-то контекст нужно передать. И как его передать — есть разные варианты. Можно параметром метода. Можно его заинжектить в этот класс, в сервис. Можно инжектить через конструктор. Можно сделать так, чтобы он просто дергал какую-то статическую переменную или статический метод откуда-то. Или грузил бы их из базы, черт побери. То есть разные есть варианты. И когда ты начинаешь в тесте делать setup, как Иван упомянул уже, ты начинаешь в этот момент продумывать: а как наиболее удобным способом передать туда все эти вещи?
Т. Валеев:
– Кстати, ты очень интересную вещь сказал: «Статические методы». То есть у меня есть какой-то статический где-то контекст снаружи. И может оказаться, что действительно я напишу этот свой метод findClass, у него будет только один строковой параметр, и я подумаю: «Вообще-то в таком режиме с этим методом тоже можно работать». То есть реализация возможна, если я где-то из глобального контекста смогу достать, какой у меня проект, JDK и так далее. Но со временем может оказаться, что поддержание этого глобального контекста несет больше накладных расходов. Просто в каком-то конкретном месте становится непонятно уже, какой сейчас контекст. Тут мультитредовость всплывает: у меня в одном потоке – один контекст, в другом потоке – другой контекст. То есть начав из соображений сделать как удобно использовать, если я буду слишком сильно сильно этому уделять внимание, может оказаться, что реализация станет вообще неподдерживаемой.
А. Солнцев:
– Ты все правильно назвал. Есть такие проблемы. Именно так во многих проектах происходит. И я как раз хочу подчеркнуть, что тест позволит это выявить гораздо раньше, как только ты в своем тесте поймешь, что тебе теперь перед запуском теста нужно каким-то образом инициализировать глобальный контекст. Какую-то статическую переменную делать. Ты сразу увидишь: «Ой, это неудобно. А вдруг я запущу два разных теста и каждый должен инициироваться по-своему, например, параллельно?» И вместо того, чтобы статическую переменную инициировать, как-то лучше и удобнее передать это, допустим, как параметр. Тест это выявит моментально. В том-то и дело.
JUG.ru Group:
– Да, из моей практики: если код становится трудным для юнит-тестирования, то чаще всего это происходит именно из-за каких-то глобальных контекстов. Тут-то и звучит сигнал, что API мы спроектировали не очень разумно, даже не с точки зрения удобства, а именно с точки зрения общей правильности. Именно так проблемы и возникают. А если мне надо запускать тест для разных глобальных контекстов, а мне трудно их засетапить? Если, чего доброго, окажется, что юнит-тест вообще зависит от того, какое программное окружение установлено на машине у разработчика? Так и вскрывается проблема, что мы что-то не продумали. Что какие-то вещи, которые мы должны бы были передавать как параметры, например, они, действительно, зависят от глобальных контекстов.
Т. Валеев:
– Но с последними репликами я совершенно не спорю! Вы не забывайте, что я абсолютно не против юнит-тестов. Я горячо за юнит-тесты. То есть с последними репликами в контексте того, что тесты и код у нас уже есть, и мы в какой-то момент понимаем, что что-то не так — я абсолютно согласен. Тест и код должны быть. Но у нас разногласия лишь по поводу того, в каком порядке они должны появиться. Позиция Андрея, как я понимаю, в том, что если написать сперва тест, то мы немножко сдвинемся в сторону удобства использования. Моя позиция в том, что если сперва не писать тест, то мы будем сдвинуты в сторону удобства разработки.
А. Солнцев:
– Не будем. Не будем сдвинуты. Мы не будем никаким образом сдвинуты в сторону удобства разработки. Это неправда. Написание теста раньше никаким образом не мешает разработке, никаким. Это — false assumption.
Т. Валеев:
– Хорошо, у тебя мнение такое, а у меня — другое.
Дешевле, качественнее — или сразу и то, и другое?
JUG.ru Group:
– Перед нашим разговором я думал, что он может перейти в такую фазу, когда каждый будет настаивать на своей субъективной позиции. Но возможен ли тут объективный взгляд, можно ли измерить как-то продуктивность команды, работающей так или по-другому? Я поискал в Google и нашел ссылку на исследования, которые проводились в Microsoft и IBM. Две команды заставляли параллельно работать над одним и тем же проектом, одни работали по TDD, другие работали не по TDD. И вывод получился такой, что трудозатраты по TDD чуть выше, а качество по TDD ощутимо выше получается. То есть при несколько более высоких трудозатратах (это важный момент, они отметили, что трудозатраты на разработку по TDD выше), судя по количеству дефектов, которые пришлось потом исправлять, у TDD — значительно выше качество. То есть, возможно, выбор между TDD и не TDD – это обычный выбор между ценой и качеством.
А. Солнцев:
– Я прокомментирую насчет цены и качества. Чуть больше затрат было подсчитано на каком-то первом этапе, когда идет разработка. Но не забывайте, что проект на этом не умирает. Проект продолжает жить. Его надо поддерживать, дальше как-то развивать. И проект, который с худшим качеством, потребует гораздо больше времени и сил на поддержку, больше багов будет, сложнее будет рефакторинг и так далее. И поэтому в конечном итоге через некоторое количество времени он окажется дороже. Поэтому говорить о том, что при TDD будут выше затраты – некорректно. В долгосрочной перспективе они меньше. Это то же самое, что прямо здесь и сейчас купить дешевле пальто. TDD – это дорогое пальто. Прямо здесь и сейчас ты купишь без TDD дешевое пальто: да, сейчас кажется, что это дешево, но через два года ты обнаружишь, что ты каждый год должен покупать новое пальто.
JUG.ru Group:
– Если только ты не настолько крут, что умеешь сразу написать прекрасный код и без TDD?
Т. Валеев:
– Я, конечно, не пишу сразу прекрасный код. Я пишу сразу более-менее неплохой код, потом пишу юнит-тест. И после этого я этот более-менее неплохой код исправляю в соответствии с упавшими юнит-тестами. И потом отправляю его также на code review и дорабатываю после этого.
А. Солнцев:
– Но, тем не менее, получается, что многократные какие-то изменения все равно есть так или иначе?
Т. Валеев:
– Многократные изменения в любом случае будут. Какая разница? Напишешь ты тест сперва, после этого ты напишешь код. После этого ты все равно должен запустить эту связку. Ты обнаружишь, что тест не проходит, будешь исправлять. То же самое и у меня, просто я сперва пишу код, потом тест. А потом запускаю, дальше то же самое. Думать, что ты пишешь, все равно придется.
Удержание задачи: «в голове», на бумаге или в тестах?
А. Солнцев:
– Я хотел бы сказать, в чем я вижу основную пользу TDD. Я вижу в этом инструмент, который помогает думать. То есть в качестве альтернативы TDD можно, например, заранее продумать в голове, что ты будешь делать. И я полагаю, что у многих людей именно это получается. Они считают: «Зачем мне писать тесты вперед, я ведь могу и в голове сразу все продумать». Может быть, Тагир как раз так считает. И, может быть, это годится до определенного момента, пока не очень много изменений и когда хорошая свежая голова. Действительно, в голове можно довольно детально все продумать, но начиная с какого-то момента голова больше не справляется. Второй вариант – нарисовать на бумажке заранее, и может быть еще вариант — с кем-то обсудить. То есть это — все варианты, позволяющие заранее продумать, что ты будешь писать, но, на мой взгляд, из них юнит-тест самый лучший, потому что он наиболее близок к коду. Все равно: если ты думаешь в голове и тебе кажется, что все продумал идеально, то когда садишься и начинаешь писать код, выясняются нюансы, о которых ты не подумал. То же самое на бумажке: нарисовал все подробно, начинаешь писать код, выясняются нюансы, о которых ты не подумал.
Т. Валеев:
– Я пример с findClass(...) как раз и приводил, чтобы показать, что даже если ты напишешь тест, все равно выясняются нюансы, о которых ты не подумал.
А. Солнцев:
– Ты прав: да, нюансы, конечно, в любом случае выясняются, бесспорно. И когда выясняется, что ты о чем-то не подумал — вариант какой? Вернуться и назад дальше в голове продумать заново? Голова просто распухнет и сломается, не сможет так много думать. Вариант вернуться и дальше перерисовать все на бумажке? Это вариант, в принципе, но на мой взгляд, гораздо эффективнее и быстрее вернуться к юнит-тесту и его дописать. Это будет проще, чем перерисовывать все на бумажке. Проще, быстрее, эффективнее. Я хочу провести аналогию: юнит-тест, бумажка и продумывание в голове — это примерно одинаковые инструменты.
Т. Валеев:
– Если задача слишком сложна, чтобы ее целиком удержать в голове, то я не думаю, что TDD тут возьмет и магическим образом всех спасет. Задачу нужно разбивать на подзадачи. Решил маленькую подзадачу, протестировал ее — хорошо. Этот кусочек у тебя в голове уже не детализирован, он превратился в работающую абстракцию. Дальше ты работаешь над каким-то новым кирпичиком, который использует предыдущий.
А. Солнцев:
– Это тоже метод. Я совершенно с этим не спорю. Да, надо разбивать. Никакого противоречия. Но дело в том, что когда ты в свою голову помещаешь очень много таких маленьких кирпичиков за день, голова гораздо быстрее устает. Вопрос именно в том, где держать этот workspace: в голове, на бумажке или в тесте.
Ассерты в коде — TDD или нет?
JUG.ru Group:
– Я сам далеко не все делаю по TDD, хотя и стремлюсь к этому. Но в моей практике случались какие-то сложные задачи, которые я бы не «раскусил», если бы сразу не решал их при помощи TDD. Есть задачи, решение которых, как я себе представляю, выглядит как взаимодействие большого числа маленьких «шестеренок». Алгоритмически или структурно сложные задачи. Сначала я должен быть абсолютно уверен, что каждая «шестеренка» работает правильно. Потом я пытаюсь запустить эти «шестеренки» в какой-то сложный механизм и добиваться их верного взаимодействия на более высоком уровне. Возможно, это вопрос внутренней убеждённости, но я уверен, что некоторые задачи без TDD никак бы у меня не решились. Но это, возможно, у кого как. У кого-то, может быть, решились бы. Вот вы, Тагир, как решаете наиболее алгоритмически сложные задачи?
Т. Валеев:
– Основный подход – это, естественно, разбиение задачи на более простые. То есть нужно выделить какие-нибудь шаги. На каждом шаге у вас должны быть инварианты. Это некоторые утверждения, некоторые ассерты, которые на этом шаге должны быть верны. Может быть, даже проще это запрототипировать не с помощью юнит-тестов, а расставив ассерты в коде, что в этом месте у меня такой-то инвариант верен, в этом месте – такой-то.
А. Солнцев:
– Так это же то же самое. Ассерты – это то же самое, что тесты.
JUG.ru Group:
– Если мы продумываем инварианты и превращаем их в ассерты в коде, то это того же рода работа, что создание ассертов в юнит-тестах. А вы используете ассерты в коде?
Т. Валеев:
– Я их использую именно в случае алгоритмически сложных задач. И, как правило, я их все-таки удаляю. Но это зависит от ситуации, от общей политики проекта. Например, в своей библиотеке StreamEx у меня были прямо зубодробительные куски, где нужно было распараллеливать, и чтобы все это было правильно. Был миллион частных случаев, когда в каком порядке thread’ы могут прийти к определенной точке. Я расставлял помимо юнит-тестов ассерты, для тестирования не только внешнего API, но и каких-то внутренних кусков. Все это дебажил вдоль и поперек. Когда нужно было делать релиз, я ассерты убирал. Делал я это по некоторым причинам, в том числе потому, что они могут замедлять, если они разрешены в runtime.
JUG.ru Group:
– И ещё они могут вносить side effects.
Т. Валеев:
– Ну, это плохие ассерты, если они вносят. А вот производительность они могут просадить. Если человек на весь проект держит “-ea”, то зачем ему внутри моего кода сработавший ассерт? Он все равно с него пользы не получит.
А. Солнцев:
– Да, я соглашусь с тем, что ассерты – это ведь на самом деле те же самые юнит-тесты, просто в другом месте написанные. Только с юнит-тестами плюс в том, что их не надо удалять, и они никак не влияют на производительность. А раз ты пишешь ассерты, а потом и юнит-тесты, значит, ты двойную работу делаешь?
Т. Валеев:
– В процессе прототипирования иногда проще написать ассерты, потому что когда прототипируешь алгоритм, ассерт можно, например, внутрь цикла поставить, а потом уже понять, как, например, тело этого цикла вынести в отдельный метод и его открыть для юнит-теста.
А. Солнцев:
– Так мы на самом деле говорим ведь почти об одном и том же, просто чуть-чуть разными словами это называем. Но на мой взгляд, юнит-тест правильнее, чем ассерт, хотя бы потому, что ты заранее подумаешь об этом. И потом не придется переделывать.
Т. Валеев:
– Если речь идет не о внешнем API, а о внутренней структуре алгоритма, то как она разбита на методы – это не очень принципиально, потому что этого никто не видит. Главное, чтобы это было корректно и быстро. И вполне может оказаться, что все те места, где ты хочешь поставить ассерты, если ты по этим точкам разобьешь на методы и поставишь юнит-тесты, то код будет слишком превращен в «лапшу» и его будет труднее воспринять. Я говорю про алгоритмически сложный код. Но тут, возможно, тоже вкусовщина. Кому-то кажется, что сто однострочных методов лучше, а кому-то — что десять десятистрочных тоже хорошо.
Прописывание интерфейсов до их реализации — TDD или нет?
Т. Валеев:
– И ещё я хочу добавить относительно проектирования API. Это, казалось бы, такая штука, для которой TDD действительно очень полезен, потому что если создается новый API, то мы сразу же будем думать о том, как его удобно использовать. И из моей практики, если я действительно в голове не понимаю, как удобно, так или эдак, когда у меня есть варианты, то тут мой подход, наверное, ближе всего приближается к классическому TDD, но все равно я не пишу тест вперед. Я пишу исключительно интерфейсы без реализации, и под эти интерфейсы пишу тесты. И эту связку сперва отлаживаю. То есть я смотрю заранее, будет ли тест удобен или не удобен. Тест должен при этом компилироваться вместе с интерфейсом. Естественно, реализации нет. А после этого уже, когда по интерфейсу мне кажется, что удобно, я могу писать реализацию.
А. Солнцев:
– Но это вполне допустимо. Мы тоже так примерно и работаем.
Т. Валеев:
– Но опять же, все равно интерфейс появляется вперед, если речь идет именно о порядке написания кода.
А. Солнцев:
– Это как раз не принципиальный момент. Это просто означает, нажмешь ты Alt + Enter чуть раньше или чуть позже, но, по-моему, от этого сильно ничего не меняется.
Т. Валеев:
– ОК! Получается, принципиальных расхождений у нас нет. То есть мы все согласны, что тестировать нужно и это очень важно, а расхождения лишь в маленьких деталях, на мой взгляд, которые, может быть, более субъективны.
А. Солнцев:
– Да. Может быть, да.
Бывает ли так, что TDD наносит вред проекту?
JUG.ru Group:
– Действительно, к некоторому консенсусу мы пришли. Главное, конечно, что в ответ на вопрос «Бывает ли на проекте вред от TDD? TDD – зло?», все-таки ответ: «Нет, не зло». А в частности, юнит-тесты сами по себе — это абсолютно точно добро. И, по-моему, все должны уяснить, что нет никакого оправдания тем, кто не использует юнит-тесты, и у кого сильно отстает реализация от покрытия юнит-тестами. Вот это — как раз зло, безусловно.
А. Солнцев:
– Да!
Т. Валеев:
– Я могу уверенно заявить, что ни в коем случае нельзя коммитить без тестов, даже если у вас локальный репозиторий. В одном коммите у вас обязательно должна быть и реализация, и тесты, которые хотя бы как-то более-менее эту реализацию покрывают. То есть тесты могут быть написаны позже, как я делаю, но закоммичено должно быть обязательно в одном коммите.
Бывает ли такое, что TDD наносит вред проекту? Так как я TDD не использую, у меня нет таких примеров, когда наносит вред. На мой взгляд, TDD – это не зло, но TDD избыточен. Мне кажется, что он не нужен, но он не зло. Если кому-то нравится, я совершенно не против. Даже если я буду работать в проекте с человеком, который использует TDD, мне будет абсолютно не жалко.
А. Солнцев:
– Я, конечно, точно скажу, что не избыточен. Из сегодняшней беседы я сделал вывод, что ты делаешь как минимум столько же движений, а может быть больше. Поэтому не могу согласиться с тем, что он избыточен. А ответить на вопрос: «Бывает ли, когда TDD наносит вред?» — мне действительно очень интересно. Я так прямо сходу не могу сказать, что у меня такие явные примеры есть, но я могу себе представить, что наверняка TDD несет вред, когда людей заставляют это делать. Например, когда они не умеют, но им сказали: «Надо». Они делали, делали, написали кучу тестов, а в конце взяли и все равно по старинке все сделали. Тогда действительно получится, что TDD – это будут избыточные трудозатраты.
Т. Валеев:
– Но это еще та вещь, которую сложно контролировать, если парное программирование не используется. То есть какой код человек написал, можно контролировать на code review. А что он написал сначала, а что потом, если не стоять у него за спиной — как ты это поймешь?
А. Солнцев:
– Это невозможно контролировать, но я скажу по моему опыту, что это всегда видно. Когда ты видишь коммит, ты всегда видишь: если тесты писались после кода, он хуже по качеству, больше зависимостей и так далее. Это видно невооруженным глазом.
Т. Валеев:
– Не знаю, тут предметно спорить довольно тяжело, наверное.
Можно ли объективно измерить выгоду или проигрыш от TDD?
А. Солнцев:
– Да, это очень субъективно, я соглашусь. Но я могу привести пример. Мы вообще парное программирование используем, но однажды я ехал один в автобусе, и мне нужно было решить сложную задачу. Задача примерно такая: клиент загружает файл с платежами, и нужно было у него проверить кучу всего, что нет повторных номеров платежей, что суммы не слишком большие и так далее. И я сделал это один в автобусе без TDD, а потом мы на следующее утро пришли и сделали с коллегой полностью по TDD с нуля все то же самое. И сравнили между собой. И разница была совершенно кардинальная.
JUG.ru Group:
– Но эксперимент не чистый. Вы же уже один раз решали задачу в автобусе. Теперь следовало бы сначала решить другую задачу по TDD, а потом без TDD, чтобы сравнить результаты.
А. Солнцев:
– Это верное замечание, да. Кстати, хорошая идея. Такой эксперимент тоже можно провести. Вообще насчет идеи сделать эксперименты — она замечательная. Очень хотелось бы сделать, но как? Ведь в одну реку не войти дважды? Как можно?
Т. Валеев:
– Даже эти результаты, про которые Иван упомянул, что в Microsoft что-то измеряли и оказалось, что TDD дает более качественный код: это все очень трудно объективно как-то сопоставить. Наверняка это были разные команды, разного уровня люди и так далее. Неизвестно, был ли это проект одинаковой сложности. Очень много переменных.
А. Солнцев:
– Да, конечно. Давайте подумаем, может быть, на какой-нибудь конференции замутим или где-нибудь еще? Было бы здорово. Эксперимент.
JUG.ru Group:
– Эксперимент в этой области – это довольно сложно. Я думал о том, как можно сравнивать производительность команд разработчиков. Если определять производительность как дробь — отношение результата к трудозатратам — то трудозатраты, как раз, можно измерить. И даже категоризировать: сколько я тратил на написание юнит-теста, написание кода, рефакторинг, баг-фиксинг и так далее. А вот с числителем все сложно. К числителю ты линейку или секундомер не приставишь. Так что вопрос о том, у каких команд в среднем выше производительность — вопрос сложный и решить его, может быть, раз и навсегда не удастся. Но вообще идея на конференции сделать что-нибудь типа лайфкодинга, когда один человек решает по TDD, а другой без TDD небольшую задачу — классная. Может быть, это было бы круто.
Ну что же, спасибо вам за разговор. Отличная дискуссия получилась.
А. Солнцев:
– Да, спасибо. Было приятно поболтать.
Т. Валеев:
– Спасибо. Взаимно!
Если вы интересуетесь тестированием, напоминаем, что 10 декабря в гостинице «Radisson Славянская» пройдет конференция Гейзенбаг (регистрация), на которой можно будет послушать следующие доклады.
- No Such Thing as Manual Testing and Other Confusions
- Appium: Automation for Apps
- Как научить роботов играть в игры?
- Hero’s Journey to Perfect System Tests — Eight Assessment Criteria for Tests’ Architecture Design
- Page Objects — лучше меньше, да лучше
- Тестирование распределенных систем
- Тестирование Android–приложения Juno с ️: CI, Unit, Integration и Functional (UI) тесты. 100% Kotlin, 90%+ RxJava, Spek, JUnit, DSL для UI тестов
- Combining manual and automated testing: process and tools
- Список покупок: что нужно не забыть при запуске JMeter-тестов
- Статический вынос мозга: что скрывают анализаторы кода?
Автор: JUG.ru Group