На часах 7:15 утра. Наша техподдержка завалена работой. О нас только что рассказали в передаче «Good Morning America» и множество тех, кто впервые посещает наш сайт, столкнулось с ошибками.

У нас настоящий аврал. Мы, прямо сейчас, до того, как потеряем возможность превратить посетителей ресурса в новых пользователей, собираемся выкатить пакет исправлений. Один из разработчиков кое-что подготовил. Он думает, что это поможет справиться с проблемой. Мы размещаем ссылку на обновлённую версию программы, пока ещё не ушедшей в продакшн, в чат компании, и просим всех её протестировать. Работает!

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

image

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

Почему я использую TDD?


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

Когда кто-то спрашивает меня о том, почему ему стоит связываться с TDD, я рассказываю ему эту историю, и могу вспомнить ещё с десяток похожих случаев. Одна из важнейших причин, по которым я перешёл на TDD, заключается в том, что эта методология позволяет улучшить покрытие кода тестами, что ведёт к тому, что в продакшн попадают на 40-80% меньше ошибок. Это то, что мне нравится в TDD больше всего. Это снимает с плеч разработчиков целую гору проблем.

Кроме того, стоит отметить, что TDD избавляет разработчиков от страха внесения изменений в код.

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

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

Что такое TDD?


TDD расшифровывается как Test Driven Development (разработка через тестирование). Процесс, реализуемый в ходе применения этой методологии очень прост:


Тесты выявляют ошибки, тесты завершаются успешно, выполняется рефакторинг

Вот основные принципы применения TDD:

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

Как TDD может помочь сэкономить время, необходимое на разработку программ?


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

Для TDD характерна определённая кривая обучаемости, и пока новичок карабкается по этой кривой, время, необходимое на разработку, может увеличиться на 15-35%. Часто именно так всё и происходит. Но где-то года через 2 после начала использования TDD начинает происходить нечто невероятное. А именно, я, например, стал, с предварительным написанием модульных тестов, программировать быстрее, чем раньше, когда TDD не пользовался.

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

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

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

video.addEventListener('timeupdate', () => {
  if (video.currentTime >= clip.stopTime) {
    video.pause();
  }
});

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

Для того чтобы проверить каждое из вносимых в проект изменений, нужно было потратить почти минуту, а я испытывал невероятно много вариантов решения задачи (большинство из них — по 2-3 раза).

Может я допустил ошибку в ключевом слове timeupdate? Правильно ли я понял особенности работы с API? Работает ли вызов video.pause()? Я вносил в код изменения, добавлял console.log(), переходил обратно в браузер, нажимал на кнопку Обновить, щёлкал по позиции, находящейся у конца выделенного фрагмента, а потом терпеливо ждал до тех пор, пока клип не будет проигран полностью. Логирование внутри конструкции if ни к чему не привело. Это выглядело как подсказка о возможной проблеме. Я скопировал слово timeupdate из документации к API для того чтобы быть абсолютно уверенным в том, что, вводя его, не допустил ошибку. Снова обновляю страницу, снова щёлкаю, снова жду. И снова программа отказывается правильно работать.

Я, наконец, поместил console.log() за пределами блока if. «Это не поможет», — думал я. В конце концов, выражение if было настолько простым, что я просто не представлял себе, как можно записать его неправильно. Но логирование в данном случае сработало. Я подавился кофе. «Да что же это такое!?» — подумал я.
Закон отладки Мёрфи. То место программы, которое вы никогда не тестировали, так как свято верили в то, что оно не может содержать ошибок, окажется именно тем местом, где вы найдёте ошибку после того, как, совершенно вымотавшись, внесёте в это место изменения лишь из-за того, что уже попробовали всё, о чём только могли подумать.

Я установил в программу точку останова для того чтобы разобраться в том, что происходит. Я исследовал значение clip.stopTime. Оно, к моему удивлению, равнялось undefined. Почему? Я снова взглянул на код. Когда пользователь выбирает время окончания фрагмента, программа размещает в нужном месте маркер конца фрагмента, но не устанавливает значение clip.stopTime. «Я — невероятный идиот, — подумал я, — меня нельзя подпускать к компьютерам до конца жизни».

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


Вот как я выгляжу, когда программирую

Если бы я писал ту программу сегодня, я бы начал работу над ней примерно так:

describe('clipReducer/setClipStopTime', async assert => {
  const stopTime = 5;
  const clipState = {
    startTime: 2,
    stopTime: Infinity
  };
  assert({
    given: 'clip stop time',
    should: 'set clip stop time in state',
    actual: clipReducer(clipState, setClipStopTime(stopTime)),
    expected: { ...clipState, stopTime }
  });
});

Возникает ощущение, что тут куда больше кода, чем в этой строчке:

clip.stopTime = video.currentTime

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

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

Смысл не в том, сколько времени занимает ввод этого кода. Смысл в том, сколько времени занимает отладка в том случае, если что-то идёт не так. Если код окажется неправильным, тест выдаст отличный отчёт об ошибке. Я сразу же буду знать о том, что проблема заключается не в обработчике события. Я буду знать о том, что она либо в setClipStopTime(), либо в clipReducer(), где реализовано изменение состояния. Благодаря тесту я знал бы о том, какие функции выполняет код, о том, что он выводит на самом деле, и о том, что от него ожидается. И, что более важно, те же самые знания будут и у моего коллеги, который, через полгода после того, как я написал код, будет внедрять в него новые возможности.

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

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

Как методология TDD научила меня писать более качественный код?


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

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

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

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

Я знал о принципе разделения ответственности задолго до того, как освоил TDD, но я не знал о том, как разделять ответственность между разными сущностями.

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

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

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

Как TDD помогает экономить рабочее время команд?


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

Этот процесс позволяет создать «заграждение», через которое способны «перескочить» лишь очень немногие ошибки. Эта защита от ошибок оказывает удивительное воздействие на весь коллектив разработчиков. Оно избавляет от страха перед командой merge.

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

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

Без этого страха процесс работы над программами оказывается гораздо более спокойным, чем прежде. Pull-запросы не откладывают до последнего. CI/CD-система запустит тесты, и, если тесты окажутся неудачными, остановит процесс внесения изменений в код проекта. При этом сообщения об ошибках и сведения о том, где именно они произошли, очень сложно будет не заметить.

В этом-то всё и дело.

Уважаемые читатели! Пользуетесь ли вы методикой TDD при работе над своими проектами?

Комментарии (44)


  1. HPro
    02.05.2019 14:06

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


    1. alatushkin
      02.05.2019 14:19
      +2

      Unitесты и тдд экономят время на нескольких этапах:
      1)на этапе разработки: сидеть в отладке получается гораздо быстрее чем раз за разом запускать систему и подводить к нужному состоянию.
      2) на этапе украшательства: когда работающий лапшекод хочется немного подписать перед коммитом.
      3) мягко подталкивают мамкиных архитекторов к более прагматичным решениям.


      Так что да — это приносит пользу и в стартапах.
      Важно только чтобы тот человек в команде кто исполняет роль Лида/навязывает всем свое мнение по любому решению — умел в solid in action, а не только на собеседованиях спрашивать. Без этого — это будет путь боли.


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


    1. khim
      02.05.2019 15:15
      +1

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

      Вопрос тестов на слишком длинные имена — это вопрос понимания: должен этот модуль о слишком длинных именах заботится… или это задача какого-то другого модуля?

      Ответьте для себя на этот вопрос — и вы поймёте в тестах какого модуля этот вариант должен быть.

      В стартапах TDD тоже отлично работает если понять принцип: тесты пишутся, в первую очередь, тогда, когда задавали себе ответ на вопрос «а что, если X» — и отвечали на него. Как-то. Неважно как. Важно что ответ не был однозначен. Если не задавали или ответ был очевиден — тесты можно писать по желанию.


    1. Horiz
      02.05.2019 16:44

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

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


    1. OnYourLips
      04.05.2019 10:35
      +1

      Есть большие сомнения на счет эффективности данной методологии в стартапах
      А почему только в стартапах? В энтерпрайзе и хайлоаде сомнений будет гораздо больше.

      Почти все описанные плюсы в статье — это плюсы юнит-тестирования, а не плюсы TDD. И автор статьи пытается сделать подтасовку, выдавая одно за другое.
      Корректно сравнивать TDD и обычное верификационное юнит-тестирование.

      Минусы TDD навскидку:
      • Мы чрезмерно увеличиваем покрытие тестами и начинаем бояться трогать код. Обычно мы не тестируем CRUD, а при TDD должны, в результате у нас будет кучка моков, проверяющих простейшие присваивания. Естественно на расчеты есть множество юнит-тестов и без TDD.
      • Такое покрытие простейших операций занимает очень много лишнего время по сравнению с обычным покрытием (без примитивных операций, потом все равно при интеграционном тестировании это будет проверено меньшими усилиями).
      • Но при этом ничего не гарантирует дополнительно.
      • И усложняет процесс разработки: мы заранее обязаны продумывать до мельчайших подробностей реализацию перед самой реализацией, а не во время реализации. В итоге приходиться сначала сильно напрягать мозг, а потом выполнять рутину, вместо работы в потоке. Это увеличивает стресс и снижает производительность


      1. ggo
        04.05.2019 10:59

        Какой-то странный подход, наверно даже популярный миф.
        Зачем писать никому ненужные тесты на CRUD?
        Пишите ровно те тесты, от которых есть польза заинтересованным лицам.
        Зачем всегда продумывать реализацию заранее?
        Двигайтесь от простого к сложному, по мере погружения в детали.
        Зачем вообще следовать каким-то правилам, если от соблюдения этих правил нет пользы? Все что не работает лично у вас и у команды, на свалку.


      1. VolCh
        04.05.2019 22:11

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

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


        1. vintage
          04.05.2019 22:30

          Тут уже страх не 3 дня дебажить, а 3 дня править тесты и контракты.


          1. VolCh
            04.05.2019 22:40

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


            1. vintage
              04.05.2019 22:46

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


        1. OnYourLips
          04.05.2019 22:49

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

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


          1. khim
            05.05.2019 00:26

            А страх появляется из-за чрезмерного покрытия.
            Слово «чрезмерное» тут лишнее.

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

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


  1. Aksigera
    02.05.2019 16:54
    +5

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

    Недавно же я сам перешел с проекта на Kotlin на проект на Node.js, со значительно меньшей и более простой кодовой базой. И теперь сам испытываю на себе тот же стресс перед мерджем в мастер — а не сломался ли undefined? Работает null? Удаленное неиспользуемое свойство в объекте — вдруг где-то было использовано? Помог ли линтер, некоторые тесты, ревью? Вполне вероятно, что нет. Линтер проверяет всякую фигню, тесты часто могут быть в стиле ASSERT_TRUE(true), а человек на код-ревью — человек. Вот бы иметь такую прогу, которая могла бы проверять семантические ошибки, опечатки, и попытки распаковать undefined зараз. И потом еще подключить эту прогу к IDE, чтобы работала в режиме real-time, вот было бы круто!

    А если всерьез, то вот мысль про качественный и быстрый код с TDD. Обдумывание будь тестов или типов перед написанием кода имеет общую природу — программист думает перед тем, как писать код :)

    Я понимаю разницу между TDD и статической типизацией, и хочу обратить внимание на их схожие стороны. В конце-концов, при наиболее полной системе типов (в языке Idris или в Haskell на стероидах), любые (поправьте меня, пожалуйста, если ошибаюсь) юнит-тесты выражаются через систему типов (dependent types).


    1. Corpsee
      02.05.2019 17:40
      +1

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

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


      1. khim
        02.05.2019 18:18
        +1

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


        1. Corpsee
          02.05.2019 19:11

          Спасибо за разъяснение, буду знать теперь.


    1. Koneru
      02.05.2019 17:43

      Что значит сломался undefined? Что значит распаковать undefined? Что значит работает ли null?
      P.s. я работаю с JS на FrontEnd. В работе использую ReactTS, но все равно не понял что вы сказали, а мысль, как я понял, частично решается подключением Typescript и линтерами. Остальное свойственно и другим языкам.


    1. khim
      02.05.2019 17:50

      В конце-концов, при наиболее полной системе типов (в языке Idris или в Haskell на стероидах), любые (поправьте меня, пожалуйста, если ошибаюсь) юнит-тесты выражаются через систему типов (dependent types).
      Теоретически — да, можно всё перенсти в систему типов. Практически — это неудобно.

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

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


      1. VolCh
        03.05.2019 09:55
        +1

        Не согласен, что статическая типизация и тесты — это из одной оперы. Они немного перекрывают друг друга (в случае слабой статической, как в Си, например, очень немного), но защищают от разных категорий ошибок на уровне модуля. Типизация (я про TypeScript прежде всего по сравнению с JavaScript) защищает в основном от ошибок неправильного использования в клиентах модулей, а модульные тесты защищают от неправильных алгоритмов внутри модуля. Толку от того, что вы на 100% уверены, что в функцию pow(a, b) передаются два числа, если внутри они перемножаются, а не возведение в степень происходит?


        1. khim
          03.05.2019 10:19

          Толку от того, что вы на 100% уверены, что в функцию pow(a, b) передаются два числа, если внутри они перемножаются, а не возведение в степень происходит?
          Если у вас все функции имеют сложность как у функции pow — то вы и без тестов и типизации их без ошибок напишите (или, наоборот: если вы можете в pow вместо возведения в степень написать умножение — то вам и TDD не поможет).

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

          Главное: эти посредники могут быть написаны разными людьми, а то и разными организациями. Историю про Mars Climate Orbiter помните, да? Все компоненты были офигительно покрыты тестами: и в космическом аппарате и в наземной части. Вот только типы данных они использовали разные…

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

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

          Современные компиляторы позволяют эту проблему обойти (см., например, std::chrono), а в TypeScript'е типы изначально довольно сильные, так что это уже сейчас — скорее вопрос привычки…


          1. VolCh
            03.05.2019 10:36

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

            Я тоже считаю, что типы и тесты дополняют друг друга, немного перекрываясь. Но я не понимаю, почему именно сначала сильную статтипизацию, потом тесты. Переход на другой язык может быть очень дорогим (кроме случаев типа JS->TS), а постепенно с монолитной архитектурой перейти будет ещё труднее. Тестами же можно начать покрывать постепенно.


            1. khim
              03.05.2019 15:32

              Переход на другой язык может быть очень дорогим (кроме случаев типа JS->TS), а постепенно с монолитной архитектурой перейти будет ещё труднее.
              И вот именно поэтому — сначала типы, потом тесты. Если у вас простенький скрипт строк на 100 — то пофиг, на чём этот скрипт.

              Если же задачи, возлагаемые на скрипт, растут и возникает желание «покрыть его тестами» — то это значит, что у вас уже не скрипт, а что-то большее.

              Обычно это происходит, когда пересечена граница 300-500 строк (по разному в зависимости от используемого языка).

              Так вот: обычно компонент такого размера — ещё можно переписать на типизированный язык… И ровно этот и нужно сделать.

              А тесты… Тесты можно и потом.


              1. VolCh
                04.05.2019 22:20

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


                1. khim
                  05.05.2019 00:22

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

                  И да — в этом случае вам придётся мириться с этим болотом.

                  При том, что никаких ошибок типов ещё не было, а баги на поведение уже есть.
                  А вы хотя бы AFL пробовли на ваш код напустить? И как вы узнаёте какие из «багов на поведение» связаны с типами, какие нет? «А не сломался ли undefined? Работает null? Удаленное неиспользуемое свойство в объекте — вдруг где-то было использовано?» — это ведь всё «закрывается» типами… а подобные вопросы я наблюдал от людей — фанатов нетипизированных языков частенько.


          1. akryukov
            03.05.2019 18:43

            Историю про Mars Climate Orbiter помните, да? Все компоненты были офигительно покрыты тестами: и в космическом аппарате и в наземной части. Вот только типы данных они использовали разные…

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


            1. khim
              03.05.2019 19:13

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


              1. akryukov
                03.05.2019 19:30

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


                1. khim
                  03.05.2019 20:30

                  В TS нету, в C++ — начали появляться.


          1. akryukov
            03.05.2019 22:24

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


            1. khim
              04.05.2019 18:36

              Если две системы общаются через (де)сериализацию, то строгость типизации при разработке каждой из них не очень поможет.
              Нужен общий IDL для двух систем. И да, там должны быть разные типы для дюймов и сантиметров.

              Тут сразу видно и как типизация может помочь и как она не может помочь: если у вас есть типы «число» и «строка», то типизация не поможет, если есть тип «длина» (который можно получить из типа «число» указав единицу длины) — тогда поможет. В C++14 появились зачатки этого подхода, скажем 1s — это одна секунда.

              К сожалению дюймов/сатиметров и килограммов — там пока нету…


  1. ukt
    02.05.2019 18:45
    +1

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


  1. vintage
    02.05.2019 18:49
    +12

    Возникает ощущение, что тут куда больше кода, чем в этой строчке:

    Там не просто больше кода, он ещё и куда более сложный для понимания.


    Как методология TDD научила меня писать более качественный код?

    Не научила. Вместо одной простой строчки, которой даже тестирование не требуется(clip.stopTime = video.currentTime), он понаписал экшенов, редьюсеров, селекторов и прочей ахинеи, и накрутил сверху ещё столько же кода для тестирования. При этом самый шик в том, что в тесте он фактически скопипастил код из редьюсера ({ ...clipState, stopTime }), чтобы этот самый редьюсер протестировать.


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

    Почему он не сделал это сразу остаётся загадкой.


    1. ggo
      03.05.2019 09:40

      Чувак подробно объясняет, чем многострочник лучше однострочника.

      Смысл не в том, сколько времени занимает ввод этого кода. Смысл в том, сколько времени занимает отладка в том случае, если что-то идёт не так. Если код окажется неправильным, тест выдаст отличный отчёт об ошибке. Я сразу же буду знать о том, что проблема заключается не в обработчике события. Я буду знать о том, что она либо в setClipStopTime(), либо в clipReducer(), где реализовано изменение состояния. Благодаря тесту я знал бы о том, какие функции выполняет код, о том, что он выводит на самом деле, и о том, что от него ожидается. И, что более важно, те же самые знания будут и у моего коллеги, который, через полгода после того, как я написал код, будет внедрять в него новые возможности.


      1. vintage
        03.05.2019 09:47
        +4

        Ой, ну всё, теперь мои тезисы не валидны, ведь он всё так подробно объяснил.


  1. kashey
    03.05.2019 02:16
    +2

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

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


  1. fshp
    03.05.2019 04:37

    Я скопировал слово timeupdate из документации к API для того чтобы быть абсолютно уверенным в том, что, вводя его, не допустил ошибку.

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


  1. Scf
    03.05.2019 09:30
    +1

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


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


    1. ggo
      03.05.2019 09:56

      еще больше времени требуется на обновление написанных тестов при крупных рефакторингах. TDD тесты, как правило, активно используют моки и поэтому хрупкие:


      Массовое обновление тестов требуется при изменении API. При измененнии внутренней реализации (что происходит чаще всего) требуется точечное изменение тестов.

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

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


      1. Scf
        03.05.2019 10:56

        что есть API? Если мы говорим про публичное API модуля, то его тесты, скорее, интеграционные.
        Если мы говорим про API отдельных классов, то оно может меняться часто и быстро.


    1. VolCh
      03.05.2019 10:18

      А можно простейший пример того как усложняется код?


      1. Scf
        03.05.2019 10:55
        +2

        например, вместо


        public void transfer(Producer p, Publisher c) {
          c.publish(p.produce());
        }

        Приходится писать:


        public void transfer(Producer p, Publisher c) {
        doTransfer(c::publsh, p::produce)
        }
        
        @VisibleForTesting
        void doTransfer(Supplier<Data> p, Consumer<Data> c) {
          c.accept(p.get())
        }


        1. vassabi
          03.05.2019 12:29

          а почему нельзя сделать что-то типа такого?

          @MockedBy(transfer_mock)
          public void transfer(Producer p, Publisher c) {
            c.publish(p.produce());
          }
          
          @VisibleForTesting
          public void transfer_mock(Producer p, Publisher c) {
            c.accept(p.get())
          }


          1. fx-man
            03.05.2019 22:00

            c — это экземпляр класса ведь какой то? А где же он объявлен? Где описаны методы? Ничего же не видно? Где код то весь?


            1. vassabi
              03.05.2019 23:53

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