Как стать автором
Обновить

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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

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

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


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

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

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

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

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

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


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

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

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

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

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


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

P.S. Кстати, почти не встречал чистого (сначала тесты, потом код) TDD по работе, обычно и код и тесты приходилось писать одновременно. Но они действительно неслабо помогают при изменениях, в т.ч. самых минимальных, когда кажется что ничего просто не может поломаться.
НЛО прилетело и опубликовало эту надпись здесь
А страх появляется из-за чрезмерного покрытия.
Слово «чрезмерное» тут лишнее.

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

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

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

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

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

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

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

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

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

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

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

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

Что значит сломался undefined? Что значит распаковать undefined? Что значит работает ли null?
P.s. я работаю с JS на FrontEnd. В работе использую ReactTS, но все равно не понял что вы сказали, а мысль, как я понял, частично решается подключением Typescript и линтерами. Остальное свойственно и другим языкам.
Что значит сломался 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.
Необычный слэнг, больше 2 лет на фронте, но не слышал. Спасибо за подробное объяснение :)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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

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


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

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

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


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

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

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


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

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


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

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

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


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

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


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

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

Нет :) Это вы предложили решить задачу сильного изменения 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(???);
  }
Хм… а как еще можно подойти к решению, если у меня в моменте скажем 12 тысяч коммерческих пользователей моего API?

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


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

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


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

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

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

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

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

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

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


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

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


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

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

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


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

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


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

Ну представьте, что был классический 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 и маппить их на однцу и туже таблицу? Или просто добавить методов, оставив публичные свойства или тупые геттеры/сеттеры, через которые кто-то рано или поздно получит "несанкционированный доступ" к кишкам сущности, обойдя заложенные в её нормальных методы гарантии инвариантов?

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

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

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

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

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

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

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

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

Может да, а может нет, например 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));
  }

Так что ли?

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

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

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

хорошо что 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));
  }
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);
    }
}

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

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

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

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


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

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


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


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

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