Деконструкция TDD +8




Здравствуйте, меня зовут Дмитрий Карловский. А вы на канале Core Dump, где мы берём разные темы из компьютерной науки и деконструируем их по полочкам. Начнём мы с разработки через тестирование.


Test Driven Development

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


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


Видео запись этого разбора.


Суть TDD


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


Pure TDD


И тут сразу возникает вопрос вопрос на миллион...


Что делать, когда тест изначально зелёный?


Варианты ответов...


  • Сломать код
  • Удалить тест
  • Это невозможно

Если сломать код, то тесты естественным образом покраснеют. А после того как мы откатим изменение, тесты снова станут зелёными.


Можно удалить тест. Ведь если нет теста, то нет и проблемы с его изначальным цветом.


Наконец, моё любимое: по TDD такого быть не должно. Где-то ты накосячил, что у тебя так получилось. Покайся, грешник.


А теперь, внимание, правильный ответ: все эти варианты — это полная беспросветная чушь. Хотя именно один из них, как правило, слышишь от адептов TDD.


Парадокс воронов


Говоря про ломание кода, нельзя не упомянуть про парадокс воронов. Суть его в том, что задаётся вопрос: "Все ли вороны чёрные?". И для ответа на него берутся нечёрные предметы. Например — красные яблоки. И каждое такое яблоко как бы подтверждает тезис о том, что "все вороны чёрные", ведь яблоки не чёрные и при этом не вороны. Что-то тут не так с логикой, не правда ли?



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


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


Изначально зелёные тесты неизбежны


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


  1. R ? G
  2. R ? G
  3. R ? G
  4. G ?
  5. G ?
  6. G ?
  7. G ?
  8. G ?

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


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


Правильный TDD


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


Fixed TDD


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


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


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


В такой форме TDD уже можно применять с пользой. Однако...


TDD приводит к куче лишней работы


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


Давайте рассмотрим типичный сценарий написания простой функции...


Итерация В начале В процесссе В результате
1 R R G
2 GR RR GG
3 GGR RRR GGG
4 GGGR GGRR GGGG
5 GGGGR GGGGR GGGGG
6 GGGGGR RRRRRR GGGGGG

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


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


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


Когда TDD полезен


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


  • Исправление дефектов
  • Заранее известный контракт
  • Не заставить себя писать тесты

Если к вам пришёл баг, то вы его можете сперва формализовать в виде теста, а далее исправлять код, пока все тесты не позеленеют.


Кроме того, в ситуации, когда контракт вам известен заранее, вы можете сразу написать все тесты, а потом уже весь код, который им соответствует. Формально, это будет не TDD, так как вы не будете менять код после добавления каждого теста. Однако, это самый что ни на есть Test Driven, так как тесты будут диктовать вам реализацию.


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


Программировать ли по TDD?


Если кто-то вам скажет, что он "программирует по TDD", то можете быть уверены, что он попросту не ведает, что творит. У TDD есть ряд фундаментальных проблем, поэтому его применение оправдано лишь в весьма ограниченном числе случаев. И то, не в той форме в которой ему как правило учат многочисленные коучи.


- Ритуализация :-(
- Явно некорректный код :-(
- Бесполезная работа :-(
- Там, где это уместно :-)
- Не зацикливаться :-)

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


Что ещё посмотреть по TDD?


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



Продолжение следует..


  • Лайк
  • Подписка
  • Комментарий
  • Поделись-ка

Если данный разбор показался вам полезным, то дайте мне об этом знать посредством лайка. А так же поделитесь ссылкой на него со своими коллегами.


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


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


Наконец, подписывайтесь на канал, чтобы не пропустить дальнейшие разборы. Нам ещё много чего с вами нужно будет обсудить.


На этом пока что всё. С вами был боевой программер Дмитрий Карловский.

Теги:




Комментарии (84):

  1. DexterHD
    /#22101136

    Статья крайне предвзята (с первых же предложений навешиваются ярлыки) и пестрит оценочными суждениями, подменой понятий и недостоверной информацией.

  2. zloddey
    /#22101254 / +2

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


    Чисто парочка самых эпичных фейлов для примера:


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

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


    1. В фазе Red: "какое поведение я хочу получить от моего кода?", или "в каком случае код поведёт себя неправильно?". Мы фокусируемся только на вопросах входа и выхода данных в функцию, но не грузим мозг качеством кода, алгоритмами, структурой...
    2. В фазе Green: "какие изменения в коде позволят пройти все тесты?". Мы снова не грузим мозг вопросами чистоты или качества кода, но зато высвободившийся мыслительный ресурс направляем на то, чтобы получить работоспособную для данного этапа версию.
    3. В фазе Refactor: "можно ли улучшить читаемость и понятность этого кода, не меняя его поведения?". Имея на руках работающий код и работающие тесты, мы можем попробовать улучшить их читаемость. BTW, тесты тоже можно и нужно рефакторить, но только сам код при этом должен оставаться неизменным.

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


    Изначально зелёные тесты неизбежны

    Типичнейшее заблуждение новичка: раз я написал тест, и он сразу зелёный — значит, всё хорошо! Зелёный цвет хороший, красный цвет плохой. Я написал пачку тестов, они все покрашены в правильный цвет — значит, мой тестируемый код работает правильно. А тот, кто требует делать их изначально красными, тот требует соблюдать бессмысленные ритуалы и делать лишнюю работу.


    Но смысл здесь очень даже присутствует. Есть много причин, по которой тест может быть зелёным. Например, в нём забыли поставить ассерты. Я видел такое неоднократно. В тесте просто повыполнялся какой-то код и не упал с исключением — вот и славно. Несложно понять, что такой "тест" довольно слабо будет фиксировать поведение кода. Или проверка некорректная: по логике ждём падения в одном месте, а на деле оно происходит совсем в другом (помню случай, когда нашёл у коллеги несколько таких тестов, которые вроде бы покрывали разные кейсы, но на деле дружно ловили совсем другое исключение, одно и то же для всех тестов). Или проверка проходит успешно, но только потому, что не успевает выполниться соседний поток.


    Наличие таких "вечнозелёных" тестов приводит к даже куда более худшим результатам, чем отсутствие тестов вообще. Они порождают ложную уверенность, будто бы в коде всё хорошо. Маскируют ошибки. Ведут ложной дорогой разработчика, который ищет причины багов.


    Так что, если в TDD и есть какие-либо правила, которые надо выполнять "ритуально", так это одно: никогда не доверяй тесту, если не видел, как он падает. Правило, которое "выведено кровью". Надо проверять сам тест: он должен тестировать именно то, что от него ожидается. А для этого надо убедиться, что его "лампочка загорится", если код работает неверно. В какой момент это будет дешевле и быстрее сделать? Когда тест есть, а кода ещё нет.


    Ну ок, бывает и так, что код уже существует на момент написания тестов. Что тут делать? Временно "сломайте" код, что бы и нет? Хороший способ. Либо временно инвертируйте тестовый ассерт. Тоже годится. В момент написания это обычно сделать гораздо дешевле, чем когда-то потом.


    Я мог бы ещё много чего критического написать, но уже достаточно. Главное, что хочется пожелать читателям сего опуса: читайте первоисточники, хоть на английском, хоть в переводе (уже много лет есть). Думайте своей головой. Обязательно практикуйтесь и анализируйте свои впечатления. Это навык, который требует большого объёма практики, но и в ответ даёт многое!

    • nin-jin
      /#22101342 / +1

      Например, в нём забыли поставить ассерты. Я видел такое неоднократно.

      Видимо автор такого теста слишком много думал над ритуалами, но мало над тем, что он делает.


      В тесте просто повыполнялся какой-то код и не упал с исключением — вот и славно.

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


      Временно "сломайте" код, что бы и нет? Хороший способ.

      Почитайте про парадокс воронов внимательнее. Ломая код вы проверяете лишь, что яблоки красные.


      Либо временно инвертируйте тестовый ассерт.

      А это уже тот самый бесполезный ритуал, который не даёт вам никакой дополнительной информации.

      • zloddey
        /#22102052

        А это уже тот самый бесполезный ритуал, который не даёт вам никакой дополнительной информации.

        Ох уж эта самоуверенность молодости теоретика!


        Инвертируя ассерт, я проверяю две вещи:


        1. В процессе выполнения теста мы доходим до ассерта
        2. Результат работы ассерта влияет на результат всего теста

        Как ни странно, на практике оба эти утверждения верны далеко не в 100% случаев. Тест может быть написан таким образом, что до целевой проверки дело не доходит, и из кода это может быть совсем не очевидно. Или же результат проверки определённым образом игнорируется: ловится без обработки, выпадает в боковом треде, и т.п.


        Не раз и не два я получал реальную пользу от этого "бесполезного ритуала", находил благодаря им и исправлял ошибки в тестовой системе до того, как код отправлялся в прод. Потому-то и смешно читать подобные статейки, особенно со столь радикальными выводами. К сожалению, подобными поспешными выводами автор расписывается лишь в собственной неграмотности.

        • nin-jin
          /#22102574

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

          • zloddey
            /#22103298

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

            Читаю этот комментарий со слезой умиления. Разбираешь человеку ситуацию по деталям, пережёвываешь, кладёшь в рот — а он в ответ: "так просто ты должен научиться писать тесты правильно! это же очевидно!". Конечно, очевидно! Я ведь не просто так провёл всю сложную работу, сначала выясняя проблему в коде, а потом подбирая точные и понятные слова, чтобы изложить её здесь.


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


            Требования к понятности, читаемости тестов приводят к необходимости прятать детали реализации под капот, и не всегда бывает возможно быстро до них добраться. Да это и не надо в большинстве случаев. Понятность и модифицируемость самих тест-кейсов обычно бывают важнее. Но в некоторых (заранее неизвестных!) случаях это выходит боком, если дополнительно не проверить. Порой и многопоточность оказывается деталью реализации тестов или тестируемой системы. Гораздо проще для таких помнить и применять один простой rule of thumb ("убедись, что тест падает на ассерте"), чем полностью анализировать каждый раз весь flow кода от места вызова до ассерта.


            тестирование тестов тоже можно автоматизировать через инструменты мутационного тестирования

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


            Может, я что-то не так делал? Просветите, пожалуйста!


            Хотя, если не сможете просветить, я всё понимаю. Кидаться какашками в TDD куда проще.


            Я, конечно, пороха не нюхал

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

            • nin-jin
              /#22103538

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


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


              Про мутационное тестирование есть статьи в том числе и на Хабре. Я не нахожу его полезным. Но вам возможно пригодится, если применять его к тестам, а не тестируемому ими коду.


              У вас сарказметр барахлит. По поводу тестирования могу порекомендовать: Фрактальное тестирование.

      • zloddey
        /#22102084

        Видимо автор такого теста слишком много думал над ритуалами, но мало над тем, что он делает.

        vs


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

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

        • nin-jin
          /#22102604

          В первом случае речь шла про "забыл", во втором про "отсутствие асерта". Я же процитировал конкретные реплики, на которые отвечал. Отсутствие асерта — нормальная ситуация. Но забыть его — это всё равно, что забыть голову.

      • zloddey
        /#22102090

        Почитайте про парадокс воронов внимательнее. Ломая код вы проверяете лишь, что яблоки красные.

        Ломая код, я, на минуточку, проверяю, что тест выполняет свою непосредственную функцию — красит результат выполнения (а не абстрактные яблоки) при наличии ошибок в коде. Если Вы считаете, что это бесполезное занятие, то, может, вообще не стоит автотесты писать? Ну так вперёд и с песней, дорогой мой!

        • nin-jin
          /#22102666

          Вы "проверяете" тест не на ошибках (непреднамеренных дефектах), а на преднамеренных дефектах. Это два почти не пересекающихся множества дефектов.

          • VolCh
            /#22106070

            Для некоторых ошибка и дефект в данном контексте синонимы. У тестов нет цели выявлять не внимательны или нкдальновилныз разработчиков, игнорируя саботажников. Их назначение выявлять некоторые дефекты в коде, неожидаемое его поведение.

            • nin-jin
              /#22107050 / +1

              Вот именно, в коде — имплементации ожидаемого поведения, а не в белиберде.

    • vep
      /#22101462 / +3

      тесты тоже можно и нужно рефакторить, но только сам код при этом должен оставаться неизменным
      Если я правильно понимаю TDD, то переход RED->GREEN нужен в том числе и для проверки самих тестов. Если у меня тесты зелёные, затем я их «отрефакторил», а они всё ещё зелёные, появилась ненулевая вероятность, что они будут зелёные даже со сломанным основным кодом.

  3. CrazyOpossum
    /#22101434

    Говоря про ломание кода, нельзя не упомянуть про парадокс воронов. Суть его в том, что задаётся вопрос: «Все ли вороны чёрные?». И для ответа на него берутся нечёрные предметы. Например — красные яблоки. И каждое такое яблоко как бы подтверждает тезис о том, что «все вороны чёрные», ведь яблоки не чёрные и при этом не вороны. Что-то тут не так с логикой, не правда ли?

    Так что не так с логикой-то? И как правильно доказывать что «все вороны чёрные»?

      • CrazyOpossum
        /#22104090

        Я дочитал до конца, а вы? Если у нас есть бездонный мешок со всеми предметами в мире и мы наугад что-нибудь достаём, то каждое красное яблоко подтверждает что "все вороны чёрные".

        • nin-jin
          /#22105684

          Про Байеса в конце там глупости, конечно, написаны. Любая конечная выборка из бесконечного числа объектов вообще никак не увеличивает вероятность истинности гипотезы.

          • CrazyOpossum
            /#22108544

            Хорошо, тогда как доказать, что все «вороны чёрные»? Или даже, что «завтра солнце встанет на востоке, а сядет на западе»?

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

  4. dagen
    /#22101694 / +1

    Лёгким движением руки nin-jin vintage превращается… vintage превращается… в fillpackart!

    • Alexufo
      /#22101734

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

  5. aragaer
    /#22102190 / +2

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


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


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


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

    • nin-jin
      /#22102652

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


      При отсутствии кода тест даже не скомпилируется. Такое себе "усиление".

      • aragaer
        /#22102810 / +1

        Это предельный вариант TDD — сначала мы тестируем, что код есть. Тест падает. Пишем заголовок для кода, но не тело. Часть теста проходит, а дальше все равно падает. Пишем захардкоженную константу. И так далее.


        http://wiki.c2.com/?TransformationPriorityPremise — иногда я в таком стиле развлекаюсь с кодом.

        • nin-jin
          /#22102864

          Подумайте вот над чем: когда вы заранее знаете цвет теста, то его запуск не даёт вам никакой дополнительной информации. А если его не запускать на промежуточных редуцированных исходниках, то и писать промежуточные версии нет никакого смысла.

          • aragaer
            /#22102918 / +2

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


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

            • nin-jin
              /#22102954 / +1

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

              • aragaer
                /#22103004 / -1

                Но мне нравится запускать тесты и видеть как что-то происходит. И кроссворды разгадывать тоже нравится.

                • nin-jin
                  /#22103058 / -1

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

                  • aragaer
                    /#22103094 / +2

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


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

                    • nin-jin
                      /#22103166

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

                      • aragaer
                        /#22103178 / -1

                        По поводу причины проблем я хожу к специалисту, спасибо за заботу.

          • vsh797
            /#22104754

            Лично у меня бывали случаи, когда я был уверен, что тест будет красным, а он почему-то проходил. Не все вы можете полностью контролировать. Иногда в ритуалах все-таки есть смысл.

          • VolCh
            /#22106078

            Бывает, что думаешь, что знаешь цвет теста, а на самом деле ошибаешься. Пишешь new User, перед тем как создать класс User а оказывается где-то в зависимости он уже есть и умная IDE его и портировал незаметно.

  6. ihost
    /#22102674 / +2

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


    Если взять хотя бы разработку приложения, которое хотя бы минимально поддерживает highload, не говоря уже о настоящих HPC-решениях — то все это TDD абсолютнейшая фикция, которая никак не коррелирует с работоспособностью программы


    Почему? Да потому, что выдернутая изолированно функция и написанная по TDD будет конечно работать, но в составе самого HPC/highload-приложения это никогда не зарабоает. Потому что не только приемлемая скорость работы, а вообще работоспособность зависит от выравнивания линеек кэша и его когерентности между numa-нодами, примитивов синхронизации и барьеров памяти, аспектов спекулятивного выполнения и еще тонны вещей, о которых адепты TDD на модных языках никогда и не слышали скорее всего

    • nin-jin
      /#22102716 / +1

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

      • aragaer
        /#22102802 / +1

        Ну… да, TDD в применении к модульному тестированию. Но так или иначе, к тесту, который никогда не падал, у меня доверия особо нету. Если проверку на "ну вот мы сломали, убедились, что тест это обнаружил, вернули на место" провести трудно, придется жить с надеждой на то, что тест действительно существует для того, чтобы что-то обнаруживать. Тест, который ни разу ничего не обнаружил, вызывает подозрения.

        • nin-jin
          /#22102898

          Ну вот я написал тест, который никогда не падал:


          assert( Math.pow( 2, 2 ) === 4 )

          Вы ему не доверяете?

          • aragaer
            /#22102926 / +1

            Это не тест, это некий ассершн.
            Вот если бы это было что-то вроде...


            def test_uber_method_returns_zero_when_no_users_online(self):
                self.assertEqual(2*2, 4)

            • nin-jin
              /#22102984

              Это тест. И вы не ответили на вопрос.


              Ну а ваш код прямо вот, если не запустить, то не видно, что он не тестирует то, что декларирует?

              • aragaer
                /#22103000 / +1

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


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

                • nin-jin
                  /#22103084 / -1

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

                  • aragaer
                    /#22103172 / +2

                    Любое действие, которое помогает проявить ошибку, имеет смысл. "Сломать код, чтобы увидеть, что тест сломался" позволяет проявить ошибку в тесте, если вдруг я ее там допустил. Особенно если сопоставить простоту этого действия с трудностью поиска ошибки, если она действительно допущена.

                    • nin-jin
                      /#22103214 / -1

                      Там, где светло, искать ключи, конечно, проще, чем там, где потерял. Правильно ли я понимаю, что примера правдоподобного кода теста, где ритуал бы чем-то помог, от вас ждать не стоит?

                      • aragaer
                        /#22103284

                        К сожалению нет, честных примеров такого я найти не смогу.

                        • aragaer
                          /#22103572

                          Конкретно по ссылке кода нет, код живет тут: https://github.com/aragaer/yama_android/blob/master/src/androidTest/java/com/aragaer/yama/ListActivityTest.java#L237


                          Замена onView на onData превращает тест во "всегда зеленый".

                          • nin-jin
                            /#22103604

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

                            • aragaer
                              /#22103620

                              Там не коллбэки, это в espresso так ассерты выглядят. А проблема заключалась в том, что селекторы onView и onData имеют разное применение, официально рекомендовался именно onData с очень размытым объяснением почему именно он. Но для данного конкретного теста требовался именно onView.

                              • nin-jin
                                /#22104296 / +1

                                Проблема скорее в кривом фреймворке тестирования, где асерты неявно приводят к сайдэффектам.

                            • lair
                              /#22103634 / +1

                              Асерты нельзя вызывать в колбэках.

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

                              • nin-jin
                                /#22104322

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

                                • lair
                                  /#22104826

                                  Чтобы это понять достаточно включить голову.

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


                                  Ну или учиться у того, у кого голова включена, а не у того, кто учит красить тесты в разные цвета.

                                  … а вы почему-то считаете, что это взаимоисключающие вещи.


                                  Сегодня колбэк вызывается и сломанный код фейлит тест. Завтра перестанет вызываться, но тест не свалится.

                                  Это значит, что у вас не хватает теста на то, что колбек вызывается. Который, что характерно, тоже можно начать с "не вызовем колбек и удостоверимся, что тест падает".

                                  • nin-jin
                                    /#22105176

                                    Ну если по этой дорожке пойти дальше, можно оказаться в пункте

                                    Хороший тест не делает ничего сложного, в отличие от тестируемого кода.


                                    Это значит, что у вас не хватает теста на то, что колбек вызывается.

                                    Тогда вам придётся писать минимум в 2 раза больше тестов: один для проверки, что колбек вызывается при заданных GIWEN+WHEN, а второй проверяющий собственно THEN. Смысла в этом нет, если можно можно просто написать нормальный тест.

                                    • lair
                                      /#22105186 / +1

                                      Тогда вам придётся писать минимум в 2 раза больше тестов

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

                                      • nin-jin
                                        /#22105208

                                        Обязательно. В общем случае вы не знаете от каких действий внутри теста зависит будет колбэк вызван или нет.

                                        • lair
                                          /#22105222

                                          Обязательно.

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


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

          • zloddey
            /#22102998

            Конечно, не доверяю. В каком контексте выполняется этот тест? Это IDE, блокнот, консоль браузера? Я точно получу информацию о фейле, когда тот произойдёт? Или, может, у меня упадёт билд, когда я его запущу? Или упадёт настройка и запуск тестируемого приложения?


            Такие вещи гораздо полезнее узнавать на самом-самом старте цикла TDD, а не в тот момент, когда время потрачено на что-то более-менее серьёзное. Поэтому я реально часто начинаю процедуру разработки с тестов типа такого (особенно на новых проектах):


            def it_works():
                assert False

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

            • nin-jin
              /#22103138 / -1

              Я понимаю ваше желание доказать свою правоту, но перескакивая с обсуждаемой "проверки очередного поведения очередного тестируемого объекта" на утрированное однократное "знакомство с фреймворком тестирования" вы этого определённого не добьётесь. Скорее может сложиться впечатление, что вы вообще не знакомы с ТДД.

          • lair
            /#22103424

            Я вот точно не доверяю. В основном потому, что я понятия не имею, что он тестирует и зачем.

  7. khablander62
    /#22102866 / -1

    Статья из цикла «слышал звон, да не знаю где он».

    • JustDont
      /#22103048 / +1

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

  8. vsh797
    /#22104820

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


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

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

  9. VolCh
    /#22106104

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

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


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

    • nin-jin
      /#22107110

      Цель тестирования — проверить все кейсы. тдд — методология написания тестов (код, помогающий тесту позеленеть — лишь побочный артефакт). Как вы правильно заметили, это настолько плохая методология, что не выполняет свою основную задачу, а подменяет её на… ритуализацию.


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

      • ynlvko
        /#22107322 / +1

        Вы повесили ярлык «ритуал» и пытаетесь всё показывать в отрицательном контексте.

        >адепты тдд никогда не обосновывают почему делают то, что делают — только в очередной раз повторяют заученный цикл
        Но ведь неправда! Вам уже несколько раз указали, что сталкивались со случаями, когда новый тест, который, по идее, должен падать, проходит. Если бы сначала было написано поведение, под которое был написан этот тест, а тест, оказывается, проходит и без нового кода, значит тест был написан с ошибкой, и по факту он ничего не тестирует, и если новый код ведёт себя неправильно, то программист об этом узнает уже от тестировщика или конечного пользователя. И не надо тут опять это «надо включать голову». Все делают ошибки, даже очень тривиальные.

        Это Вы почему-то всеми правдами и неправдами пытаетесь отстоять своё очень субъективное мнение о ненужности TDD. Лично мне оно жизнь спасало не раз.

        • nin-jin
          /#22107540 / +1

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

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


          тест, оказывается, проходит и без нового кода, значит тест был написан с ошибкой, и по факту он ничего не тестирует

          Это неверное умозаключение. В статье я объяснил почему.


          Лично мне оно жизнь спасало не раз.

          Обратите внимание, что я нигде не апеллирую к личному опыту.

          • ynlvko
            /#22116556 / -1

            я нигде не апеллирую к личному опыту

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

            • nin-jin
              /#22117754

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

      • lair
        /#22107366 / +2

        тдд — методология написания тестов

        Вообще-то нет. Даже в названии видно "Test-driven development". Или в Википедии: "Test-driven development (TDD) is a software development process".

        • nin-jin
          /#22107480

          На заборе тоже много чего написано. По сути же это совмещение тестирования с разработкой. При этом тестирование первично.

          • lair
            /#22107864

            На заборе тоже много чего написано. По сути же это совмещение тестирования с разработкой.

            Это не "по сути", это как вы это прочитали. Способов определить, чье прочтение "более верное", я не вижу.

      • VolCh
        /#22109310

        Как вы определяете основную задачу TDD?

    • nin-jin
      /#22107560

      Оно (она?) не про то, чтобы покрыть код тестами для всех сценариев,.

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

      • lair
        /#22107866

        как вы рефакторите без полного покрытия тестами.

        Ну как-как, берем и рефакторим. Ну да, это не самый надежный в мире процесс, но иногда это необходимо.

      • VolCh
        /#22109326

        По разному. Самое простое — средства IDE, покрытые, надеюсь, тестами :)

  10. funca
    /#22116224

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

    • nin-jin
      /#22116320 / +1

      Каким образом он может быть применим?

      • funca
        /#22118258

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

        • nin-jin
          /#22118768

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

          • VolCh
            /#22120794 / -1

            Так и бесполезность TDD не связана с его полезностью :)

  11. funca
    /#22116348 / +3

    TDD это не догма, а методология. Как любая методология она имеет свою область применимости. Для каких то классов задач это удобно, для других — нет. Я сталкивался и с теми и с другими.


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


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