company_banner

TDD: методология разработки, которая изменила мою жизнь

Автор оригинала: Eric Elliott
  • Перевод
На часах 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 при работе над своими проектами?

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Комментарии 107

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

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


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


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

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

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

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

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

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

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

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

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

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

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

                            Чем модуль у вас от компонента отличается?

                              0
                              Одно — единица кода, другое — единица функциональности.
                              Тут я расписывал подробно: habr.com/ru/post/351430
                                0

                                Пропустил как-то. Мне кажется или классификация несколько оригинальна?


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

                                  0

                                  Классификация — результат гугления и общения с сертифицированными тестировщиками.


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

                                    +1

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


                                    В первом случае я как-то могу ещё представить компонентное (в вашей терминологии) тестирование, как тестирование презентационного модуля со всеми имеющимися модулями источниками данных (пускай и шарятся они по всему приложению), а второй как? Со всеми презентационными модулями, которые его используют?

                                      0

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

                                        0

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

                                          0

                                          Границы компонентов не в коде, а в функциональности. От того, что вы поменяли одну зависимость на другую эквивалентную функциональность не меняется.


                                          И да, приложение, сервис, кластер — всё это тоже компоненты.

                                            0

                                            Так вот же функциональность: предоставлять данные и показывать данные. Где тут различие между модулем и компонентом?

                                              0

                                              "Предоставлять данные" и "Показывать предоставленные данные".

                                                0

                                                Так где в этом случае компонент, если у нас 10 источников данных, реализующих один контракт, и 10 потребителей эти данные потребляющие (для простоты — только их). 100 компонентов "каждый с каждым" или 2, зависящих от одного контракта?

                                                  0

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

                                                0

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

                                                  0

                                                  Модуль — это просто кусок исходного кода, не более.

                                                    0

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

                                                      0

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

                                                        0

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

                                                    0

                                                    Я отношусь к модулям как к таргетным объектам. Со времен ActionScript… хотя их там не было...

                              +1
                              Рефакторинг как раз поведение затрагивать не должен, рефакторинг касается только внутреннего алгоритма. И это одно из преимуществ модульных тестов — мы меняем код, но его поведение (внешнее) не меняется, и с помощью тестов мы это проверяем, получая информацию о том, если сломали и что сломали в логике модуля. Если меняется интерфейс и его логика (что Вы и подразумевали под «рефакторингом»), то придется менять не только тесты, но и те модули, которые взаимодействуют с измененным.

                              Википедия:
                              Рефа́кторинг (англ. refactoring), или перепроектирование кода, переработка кода, равносильное преобразование алгоритмов — процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения и имеющий целью облегчить понимание её работы[1][2]. В основе рефакторинга лежит последовательность небольших эквивалентных (то есть сохраняющих поведение) преобразований.


                              Под рефакторингом Вы похоже подразумеваете частичную или полную переделку неудачной архитектуры модуля, с которым таки надо как-то взаимодействовать другим. И вот здесь тесты как раз помогут понять, насколько изменилось поведение в остальных и в чем именно. ИМХО даже метод, который якобы просто всегда возвращает константное значение (сферический случай в вакууме), нужно тестировать, так как она (константа) может быть кем-то изменена и притом повлиять на функционирование других модулей.
                                0
                                И это одно из преимуществ модульных тестов — мы меняем код, но его поведение (внешнее) не меняется, и с помощью тестов мы это проверяем, получая информацию о том, если сломали и что сломали в логике модуля.
                                Это как раз если тесты модульные. А если их писали особо не думая, то в результате оно могут оказаться в виде: передать mock-объект и проверить, что два раза будет вызвана функции foo (с такими-то аргументами) и три раза функция bar (с другими аргументами).

                                Фактически код функции переписан в тест и размножен «для надёжности» с коэффициентом 10.

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

                                Зато «покрытие тестами» 100%.
                                  0

                                  Ок, давайте читать Википедию вместе :-)


                                  Рефа́кторинг (англ. refactoring), или перепроектирование кода, переработка кода, равносильное преобразование алгоритмов — процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения и имеющий целью облегчить понимание её работы[1][2]. В основе рефакторинга лежит последовательность небольших эквивалентных (то есть сохраняющих поведение) преобразований.
                                    0
                                    Ок, возможно я не совсем прав, но я себе представляю тестирование модуля именно как программы, с заглушками если есть зависимости. Т.е. перепроектирование касается именно модуля, и тесты помогают выяснить, не поломалось ли что. Если уж вся система переписывается из-за изначальных ошибок, то тесты конечно не очень помогут, но довольно редко случается, что приходится переписывать всё, так что куча модулей переходит в новую версию без изменений, если связанность небольшая.

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

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

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

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

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

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

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

                            Существует еще контрактное программирование, которое как раз частично покрывает кейсы с проверкой бизнес-логики прямо в коде приложения и тестов придется писать еще меньше. Но, насколько я понимаю, проверки контрактов сродни ассертам и отключаются в продакшене, то есть, по сути, являются своеобразными юнит-тестами.
                              +1
                              Так что никакая сильная статическая типизация и система типов не отменяют необходимость в юнит-тестировании (Хотя справедливо, что тестов скорее всего понадобится писать меньше, чем для динамически-типизированных языков).
                              На самом деле тут не зря приводили в пример Idris и Haskell. В них можно выразить в типах как раз бизнес-логику. Правда не всегда это разумно.
                                0
                                Спасибо за разъяснение, буду знать теперь.
                                0
                                В условиях отсутствия статической типизации, по моему опыту, работает правило 80/20, где 80% багов как раз разрешаются статической типизацией.

                                И применение TDD вкупе со статической типизацией получается более эффективным, потому что на оставшиеся 20% багов бизнес-логики можно выделить уже 100% тестов (подходя к их написанию с бóльшей внимательностью и затрачивая то время, которое мы сэкономили на багах типов).
                                  +2
                                  На какого типа проектов собрана такая статистика?
                                    0
                                    На проектах, где танкисты-бэкендщики вдруг решили, что они могут в js
                                      0

                                      Бэкенд этот сильно типизирован или слабо, статически или динамически?

                                      0
                                      В условиях отсутствия статической типизации

                                      На проектах на PHP, JS (node, vue, vanilla+JQuery shit frontend clients).

                                      Упреждая вопрос: нет, я не проводил замеры, и да, 80/20 — не точные цифры, и критерии оценки тоже сугубо субъективные (я бы даже сказал, сенсорные).
                                        0

                                        Ну вот почти год работаю на проекте, где на бэке PHP, а на фронте TypeScript + vue.d.ts и не могу сказать, что на бэке ошибок значительно больше. Притом тайпхинтами почти не пользуемся, за актуальностью phpdoc слабо следим, из статанализа только кодстайл и php --lint и то совсем недавно. И бэк не просто REST CRUD, скорее совсем не REST и совсем не CRUD, а ближе к SOAP.

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

                                    (Runtime) Type error: foo is undefined.

                                    Что значит распаковать undefined?

                                    (Runtime) Reference error: cannot get property bar of undefined.

                                    Что значит работает ли null?

                                    Например, что строгая проверка на значение === null не обломается о значение === undefined.

                                    частично решается подключением Typescript

                                    Именно так! Причем, насколько мне известно, TS поддерживает strictly nullable types, что делает его (статическую) систему типов даже сильнее чем в C# и Java.
                                      0
                                      Необычный слэнг, больше 2 лет на фронте, но не слышал. Спасибо за подробное объяснение :)

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

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

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

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

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

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

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

                                        Современные компиляторы позволяют эту проблему обойти (см., например, std::chrono), а в TypeScript'е типы изначально довольно сильные, так что это уже сейчас — скорее вопрос привычки…
                                          +1
                                          Типі на границе модулей дают минимальную гарантию, что всё правильно. Интеграционных тестов они не отменяют, хотя как раз уменьшают их количество в общем случае — нет нужды делать моки, проверяющие, что такой-то метод вызывается с именно с числовым параметром.

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

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

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

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

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

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

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

                                                  Мне не придётся мириться с отсутствием статических типов. Я мирюсь с их наличием :) И да, практика использования показывает, что TS не защищает от ошибок типа Uncaught TypeError: Cannot read property 'entity' of undefined (вот прямо сейчас скопировал из консоли после успешной рекомпиляции, и any практически не используется), а времени на описание типов и приведения к нужному тратится прилично.

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

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

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

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

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

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

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

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

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

                                              Ну так в типах я могу написать теорему, специфицирующую поведение pow.

                                              0

                                              Типы, кроме того, могут направлять реализацию, в отличие от. Proof search, crush tactic, вот это все.

                                            +1
                                            TDD заставляет задуматься над архитектурой, хотя бы минимально, что есть хорошо.
                                              +13
                                              Возникает ощущение, что тут куда больше кода, чем в этой строчке:

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


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

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


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

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

                                                0
                                                Чувак подробно объясняет, чем многострочник лучше однострочника.
                                                Смысл не в том, сколько времени занимает ввод этого кода. Смысл в том, сколько времени занимает отладка в том случае, если что-то идёт не так. Если код окажется неправильным, тест выдаст отличный отчёт об ошибке. Я сразу же буду знать о том, что проблема заключается не в обработчике события. Я буду знать о том, что она либо в setClipStopTime(), либо в clipReducer(), где реализовано изменение состояния. Благодаря тесту я знал бы о том, какие функции выполняет код, о том, что он выводит на самом деле, и о том, что от него ожидается. И, что более важно, те же самые знания будут и у моего коллеги, который, через полгода после того, как я написал код, будет внедрять в него новые возможности.
                                                  +4
                                                  Ой, ну всё, теперь мои тезисы не валидны, ведь он всё так подробно объяснил.
                                                +3
                                                Коллективное бессознательное давным давно записало Ерика в TDD-наркоманы, и его зацикленность на этом немного подтвержает тот факт, что он более продает себя (кому?), чем обьясняет или двигает технологию в массы.

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

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

                                                    +1

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


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

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


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

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

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

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

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

                                                          А почему нельзя написать новые тесты для нового API и реализовать этот новый API не трогая старый?

                                                            0

                                                            Например, потому что в некоторых проектах есть принцип: удалять неиспользуемый код. Плюс новый и старый API могут конфликтовать, а если старому делать префикс/суффикс legacy, то надо будет менять все тесты.

                                                              0
                                                              Например, потому что в некоторых проектах есть принцип: удалять неиспользуемый код.

                                                              Я всегда удаляю неиспользуемый код. И что? Удалили неиспользуемый код вместе с его тестами. Что тут изменять то точечно?


                                                              Плюс новый и старый API могут конфликтовать, а если старому делать префикс/суффикс legacy, то надо будет менять все тесты.

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

                                                                0

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


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

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

                                                                  Есть смысл вообще обсуждать этот случай? Или в вашем процессе допускается сломать тесты и оставить так?


                                                                  А при новых требованиях к API...

                                                                  Подождите. Мы обсуждали случай, когда старый API оставляется как есть, а новые требования реализуются в виде нового API.


                                                                  И кстати при чем здесь класс User? Если судить по имени, то это чистой воды dto. Какое поведение может быть инкапсулировано в dto? Там в принципе не должно быть ничего, кроме свойств с методами геттерами и сеттерами.

                                                                    0
                                                                    Есть смысл вообще обсуждать этот случай? Или в вашем процессе допускается сломать тесты и оставить так?

                                                                    Конечно нет. Но точечные изменения обычно в таких кейсах вносятся.


                                                                    Подождите. Мы обсуждали случай, когда старый API оставляется как есть, а новые требования реализуются в виде нового API.

                                                                    Нет :) Это вы предложили решить задачу сильного изменения API путём создания нового и оставления старого.


                                                                    И кстати при чем здесь класс User? Если судить по имени, то это чистой воды dto. Какое поведение может быть инкапсулировано в dto? Там в принципе не должно быть ничего, кроме свойств с методами геттерами и сеттерами.

                                                                    Если судить по имени, то обычно это Entity, со своим идентификатором, состоянием и поведением. Например, user.ban(), или user.changePassword(), или user.follow()

                                                                      0
                                                                      Нет :) Это вы предложили решить задачу сильного изменения API путём создания нового и оставления старого.

                                                                      Хм… а как еще можно подойти к решению, если у меня в моменте скажем 12 тысяч коммерческих пользователей моего API? Предложите что-нибудь пожалуйста. Только так, чтобы моим 12 тысячам пользователей не пришлось тратить деньги на переделку их систем.


                                                                      Если судить по имени, то обычно это Entity, со своим идентификатором, состоянием и поведением. Например, user.ban(), или user.changePassword(), или user.follow()

                                                                      user.ban() и user.changePassword() разве не обычные сеттеры? А метод user.follow() что делает?


                                                                        [TestMethod]
                                                                        public void follow_should_pass()
                                                                        {
                                                                          User user = new User("jack");
                                                                          user.follow();
                                                                      
                                                                          // Как проверим что вызов follow сработал ожидаемо?
                                                                          Assert.AreEqual(???);
                                                                        }
                                                                        0
                                                                        Хм… а как еще можно подойти к решению, если у меня в моменте скажем 12 тысяч коммерческих пользователей моего API?

                                                                        Не у всех проектов так. Не всем требуется обратная совместимость с предыдущим API.


                                                                        user.ban() и user.changePassword() разве не обычные сеттеры?

                                                                        Может да, а может нет, например ban может добавлять username в список забаненных. Или вообще всё на Event Sourcing и любой мутатор лишь эммитирует событие по коду.


                                                                        А метод user.follow() что делает?

                                                                        Полная сигнатура follow(User anotherUser) — добавляет текущего юзера в списки followers другого, а его в список following текущего.

                                                                          0
                                                                          Не всем требуется обратная совместимость с предыдущим API.

                                                                          Как так? У API вообще нет пользователей?

                                                                            0

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

                                                                              0
                                                                              Все пользователи внутренние, клиенты обновляются одновременно с сервисом.

                                                                              Хорошо. Давайте предполагать что у нас 12 тысяч внутренних клиентов, использующих старый API. С таким я тоже встречался в жизни. Вам не кажется что ситуация при этом не радикально улучшается? Вы готовы легким движением руки убить старый API, вынудив 12 тысяч клиентов перейти на новый одномоментно?


                                                                              P.S. При этом я все равно не понимаю зачем мы лезем в код старого API, если предполагается что новый у нас радикально другой? В чем целесообразность такого решения?

                                                                                0

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


                                                                                API может и радикально другой, но вот входная точка у него та же самая. Просто по семантике, предметная область не изменилась, пользователь так и остался User, никаких UserNew, UserOld или UserV2 в предметной области нет.

                                                                                  0
                                                                                  Радикально улучшается, если версия клиентов под нашим контролем на 99,999%. Не суть веб-приложение у нас, или централизованная система принудительных обновлений.

                                                                                  Я понял ваш подход.


                                                                                  API может и радикально другой, но вот входная точка у него та же самая. Просто по семантике, предметная область не изменилась, пользователь так и остался User, никаких UserNew, UserOld или UserV2 в предметной области нет.

                                                                                  Таки я не понимаю. У нас API радикально поменялся? Имена и сигнатуры методов радикально другие? Мы же не предметную область обсуждаем. Понятное дело что если мы автоматизировали управление пользователями, то в предметной области у нас пользователи и будут.


                                                                                  Беспредметный разговор в общем. Без примеров кода — это пустая трата времени.

                                                                                    0

                                                                                    Ну представьте, что был классический HTTP REST API с ендпоинтом /user(/{id}). Внутри системы представлен как анемичная модель (публичные свойства или тупые геттеры/сеттеры) User. Принято решение о создании более бизнес-ориентированного HTTP RESTish API, то есть не PATCH /user/1 {isBanned: true}, а POST /user/1/ban (без тела), и соответствующей поддержки его на уровне модели, превращении DTO в полноценную Entity. Класс User уже есть, ендпоинт /user(/{id}) уже есть, делать никому не нужное версионирование и ендпоинта и модели предлагаете? Да даже если версионирование API нужно, то предалагете создать рядом с User, который DTO, User'a который Entity и маппить их на однцу и туже таблицу? Или просто добавить методов, оставив публичные свойства или тупые геттеры/сеттеры, через которые кто-то рано или поздно получит "несанкционированный доступ" к кишкам сущности, обойдя заложенные в её нормальных методы гарантии инвариантов?

                                                                                      0
                                                                                      Класс User уже есть, ендпоинт /user(/{id}) уже есть, делать никому не нужное версионирование и ендпоинта и модели предлагаете?
                                                                                      Почему «никому не нужное»? Вам же нужное. Чтобы хоть куда-то сдвинуться.

                                                                                      Ибо у нас один такой же быстрый, как вы, попробовал один из «старых» классов, сидящих в «неправильном» неймспейсе передвинуть.

                                                                                      Ну, в общем, ему объяснили, что CL, меняющий одномоментно 284 тысячи файлов он не зальёт никогда. Даже если получит на это разрешение. Потому как к моменту, когда он отсылает этот CL на ревью кто-то где-то как-то уже успевает поиспользовать его тип «неправильным» (и неучтённым им) образом.

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

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

                                                                                          0
                                                                                          Абсолютно неважно что вам нужно менять: CL, затрагивающий 284 тысячи файлов вы не зальёте никогда.

                                                                                          Максимум, что я видел — где-то чуть больше 10 тысяч файлов одномоментно. И то — для этого пришлось долго готовиться и время выбирать.
                                                                                            0

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

                                                                            0
                                                                            Может да, а может нет, например ban может добавлять username в список забаненных. Или вообще всё на Event Sourcing и любой мутатор лишь эммитирует событие по коду.

                                                                            Это что ж? Пользователь получается должен знать о существовании списка забаненых пользователей?


                                                                              [TestMethod]
                                                                              public void should_ban()
                                                                              {
                                                                                HashSet<User> banned = new HashSet<User>();
                                                                                User user = new User(banned);
                                                                                user.ban();
                                                                            
                                                                                Assert.IsTrue(banned.Contains(user));
                                                                              }

                                                                            Так что ли?

                                                                              0

                                                                              типа того, хорошо что DI есть, а не синглтон :) Не придирайтесь к примерам из головы.

                                                                                0
                                                                                Не придирайтесь к примерам из головы.

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

                                                                                  0
                                                                                  хорошо что DI есть, а не синглтон :)

                                                                                  То есть в остальном, вы считаете что такого рода поведение удобно размещать в классе User?


                                                                                    [TestMethod]
                                                                                    public void should_add_Kent_to_followers()
                                                                                    {
                                                                                      FollowersUsers followers = new FollowersUsersImpl();
                                                                                      BannedUsers banned = new BannedUsersImpl();
                                                                                      User kent = new User(followers, banned);
                                                                                      kent.Name = "Kent";
                                                                                  
                                                                                      User bob = new User();
                                                                                      bob.Name = "Bob";
                                                                                  
                                                                                      kent.follow(bob);
                                                                                  
                                                                                      Assert.IsTrue(followers.Contains(bob));
                                                                                    }
                                                                                    0
                                                                                    final class User
                                                                                    {
                                                                                        private UuidInterface $id;
                                                                                        private string $name;
                                                                                        /** @var Collection|User[] */
                                                                                        private Collection $followers;
                                                                                        /** @var Collection|User[] */
                                                                                        private Collection $following;
                                                                                    
                                                                                        public function __construct(UuidInterface $id, string $name)
                                                                                        {
                                                                                            $this->id = $id;
                                                                                            $this->name = $name;
                                                                                            $this->followers = new Collection();
                                                                                            $this->following = new Collection();
                                                                                        }
                                                                                    
                                                                                        public function follow(User $that): void
                                                                                        {
                                                                                            if ($this === $that) {
                                                                                                throw new \InvalidArgumentException("User can't follow himself/herself");
                                                                                            }
                                                                                            $this->following->push($that);
                                                                                            $that->followers->push($this);
                                                                                        }
                                                                                    
                                                                                        public function id(): UuidInterface
                                                                                        {
                                                                                            return $this->id;
                                                                                        }
                                                                                    
                                                                                        public function name(): string
                                                                                        {
                                                                                            return $this->name;
                                                                                        }
                                                                                    
                                                                                        /**
                                                                                         * @return Collection|User[]
                                                                                         */
                                                                                        public function followers()
                                                                                        {
                                                                                            return $this->followers;
                                                                                        }
                                                                                    
                                                                                        /**
                                                                                         * @return Collection|User[]
                                                                                         */
                                                                                        public function following(): Collection
                                                                                        {
                                                                                            return $this->following;
                                                                                        }
                                                                                    }
                                                                                    
                                                                                    final class UserTest extends TestCase {
                                                                                        public function testSuccessFollow(): void
                                                                                        {
                                                                                            $user = new User(Uuid::uuid4(), 'Вася');
                                                                                            $anotherUser = new User(Uuid::uuid4(),'Петя');
                                                                                    
                                                                                            $user->follow($anotherUser);
                                                                                    
                                                                                            $this->assertContains($user->following(), $anotherUser);
                                                                                            $this->assertContains($user->following(), $anotherUser);
                                                                                            $this->assertNotContains($user->followers(), $anotherUser);
                                                                                            $this->assertNotContains($anotherUser->following(), $user);
                                                                                        }
                                                                                    }
                                                                                      0

                                                                                      То есть когда пользователь заведет себе банковский счет и начнет делать платежи, у вас в классе User появится коллекция BankAccounts и коллекция Payments?

                                                                                        0

                                                                                        Скорее да, чем нет. Может ограничусь BankAccountIds. Может только в BankAccount будет свойство User $owner, причём не факт, что тот же класс реально будет, может только строковое представление UserId будет совпадать, а может и оно не будет, а где-то будет карта соответствий пользователей и счетов, и не факт что 1:1, может 0..1:0..1, а может N:N. Всё от задачи зависит, от границ разделения ответственностей. А скорее всего эти варианты будут шагами развития системы.

                                                              0
                                                              А можно простейший пример того как усложняется код?
                                                                +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())
                                                                }
                                                                  0
                                                                  а почему нельзя сделать что-то типа такого?
                                                                  @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())
                                                                  }
                                                                    0
                                                                    c — это экземпляр класса ведь какой то? А где же он объявлен? Где описаны методы? Ничего же не видно? Где код то весь?
                                                                      0
                                                                      все вопросы по с — сюда
                                                                      я всего лишь хотел показать примерный контрпример, как «моки неусложняют код» (когда у вас в языке есть аннотации)
                                                              0
                                                              Пользуетесь ли вы методикой TDD при работе над своими проектами?
                                                              Опрос-то где?
                                                                0
                                                                Уважаемые читатели! Пользуетесь ли вы методикой TDD при работе над своими проектами?

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


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


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

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

                                                                Самое читаемое