Comments 84
Маленькие шаги - это вроде agile называется, не? Спринты по 2 недели, ценность, ретро, демо для этого и были придуманы, чтобы идти на ощупь в условиях создания чего-то нового, неизвестного и потому непонятного.
Маленькое шаги - это принцип, agile можно считать применением этого принципа по отношению к организации разработки в команде. TDD можно считать применением того же принципа по отношению к организации работы отдельно взятого разработчика - разрабатываем продукт в команде, но в конечном счёте код пишет каждый отдельно
Agile буквально переводится как гибкость или маневренность. Все перечисленное было придумано чтобы снизить цикл обратной связи, а так же чтобы с каждой обратной связью улучшать процесс и продукт.
TDD идеально вписывается в этот концепт. Обратная связь моментальная и информативная и ты не движешься дальше вперёд, пока не обработаешь и не примешь во внимание обратную связь.
Вы точно подметили сходство.
Agile и TDD — это одна и та же идея, но на разных уровнях: Agile работает на уровне продукта и команды, а TDD — на уровне кода и архитектуры.
Как правильно подметел автор статьи, многие неправильно понимают смысл некоторых вещей. Посмотрите, например, лекцию "Дядюшки Боба", одного из авторов Agile и возвожно поймете суть.
Если не хотите слушать автора, то кратко, "Гибкость" не про процессы в компании, а "гибким" должен быть сам програмист. Прошли те времена когда один пишет код, другой его тестирует, а третий думает архитектуру. Сегодня этого недостаточно. Програмист должен быть силен во всех областях и должен сам проверять свой код перед тем как отправить его в прод. От этого, между прочим может зависить чья-то жизнь. Вот про что Agile.
А можете уточнить, где именно в этом видео увязывается "agile-ность" с описанной вами профессиональной универсальностью? И/или где там вообще рассказывается про суть "agile-ности"?
Просто ваше утверждение выглядит — если говорить осторожно — сомнительно. Особенно в связке с переводом agile как гибкости. Но тратить два часа жизни (реально будет существенно больше) на проверку совсем не хочется.
Поиск по расшифровке дал ровно два упоминания слова "agile" в течение 10 секунд и в совершенно другом контексте.
Ага-ага, а еще быть гибким настолько, что сам свой код ревьюировать и аппрувить...
Меня как раз учили, что тот, кто код писал, не имеет права быть его тестировщиком. Ну то есть может протестировать, но должен быть еще кто-то с независимым взглядом. В случае TDD это ограничение обходится, но важной остается последовательность: автором кода человек становится уже после написания тестов.
P.S. К сожалению, под прикрытием напыщенного слова "гибкость" сейчас принято творить всякую дичь, а именно, быть настолько "гибким", что писать код не имея никаких явных требований, выпускать код в продакшн не тестируя его вообще, ну а дальше "сам написал, сам отправил в прод, сам доказываю, что бага есть фича".
Меня как раз учили, что тот, кто код писал, не имеет права быть его тестировщиком.
Очень правильно учили!
Ну то есть может протестировать, но должен быть еще кто-то с независимым взглядом. В случае TDD это ограничение обходится,
TDD не исключает последующего тестирования. TDD вообще к тестированию не имеет отношения — это разработка (и не обращайте внимания на устоявшийся рускоязычный перевод — он вводит в заблуждение). Разработанный через TDD код в дальнейшем должен тестироваться точно так же, как и любой другой.
Никто не говорит что код должен тестировать только программист, но программист должен протестировать его обязательно. К тому же в TDD вы тестируете только часть всей кодовой базы (в этом вся суть, юнит или максимум интеграционные тесты, быстро прогонять "по сто раз на дню"). Если в будущем софт засбоит, при помощи этих тестов можно легко локализовать проблему а не искать виновных. В профессиональной команде тесты должны писать все, и архитектор (интеграционные), и инженер по требованиям (validation/acceptance тесты) и инженеры по тестированию (boundary/negative/penetration/endurance/и т.д. тесты).
Предыдущие ответы дополню намёком: agile — это не существительное, это прилагательное.
спасибо, хорошее раскрытие техники разработки ПО.
6. Неправильное понимание цикла «тест → код → рефакторинг»
я с годами вернулся к такой последовательности, код > тест > рефакторинг > ...
Тест, написанный первым получался довольно синтетическим и был практически бестолковым.
Ваша последовательность вполне имеет право на существование. Однако это не TDD.
Ваша методика обеспечивает хорошие тесты.
Для TDD не так важно, какие в результате получаются тесты. Гораздо важнее, какая получается реализация. Такая последовательность дает стимулы, чтобы реализация была минимально необходимой для закрытия имеющихся требований.
Суть TDD — «сформулировал требование → реализовал → привел к товарному виду». Тест — это всего лишь инструмент для фиксации требований и факта их закрытия реализацией.
Вы не правы. Важна не последовательность код->тест->рефакторинг. А важна последовательность red-green-refactoring.
Кент Бек это много раз демонстрирует в своей книге. Получите красный тест, почините проведите рефакторинг снова получив красный тест.
Рефакторинг это и не только классическое изменение формы, но и собственно написание кода, который в текущем окне контекста приведёт к красным тестам. После чего он сам идёт и уточняет тесты.
В TDD не важно, что предшествует тест или код, важно пойти в цикл Green-Red-Refactoring.
Согласен: ядро TDD — именно цикл red → green → refactor. И я как раз описываю его как «тест → код → рефакторинг», а не «код → тест → …». Судя по вашей последовательности действий, это ответ скорее на вот этот комментарий: https://habr.com/ru/articles/933090/comments/#comment_28730776.
По поводу «рефакторинга, который делает тесты красными». По определению это не рефакторинг. Рефакторинг — это изменение внутренней структуры без изменения внешнего поведения, поэтому он не должен переводить зеленые тесты в красные. Если тест «покраснел», значит вы либо сломали поведение, либо начали вводить новое — и это уже новая red-фаза, которой должен предшествовать добавленный/измененный тест. См. Фаулер: https://martinfowler.com/books/refactoring.html
Кент Бек действительно показывает микрошаги, но он не смешивает фазы: красный — зафиксировать требование, зеленый — реализовать, рефакторинг — улучшить форму, сохраняя зеленый. Иногда большие изменения идут как цепочка R-G-R-G (несколько циклов), но «второе R» тут — это новый красный шаг, а не рефакторинг, меняющий поведение.
Итого: спор не о важности RGR, а о терминологии. В моем тезисе речь ровно о дисциплине: поведение задается тестом (red), код доводит до соответствия (green), улучшения формы делаются на зеленом (refactor).
Если вы меняете структуру приложения, то i9n тесты и e2e будут проходить без проблем, т.к. поведение в сумме, возможно, не поменялось. Но если вы выделили метод или целый компонент из существующего компонента и собрали новую композицию связей, то у существующего кода тесты моментально станут красными.
У всего приложения вы не поменяли поведение (возможно), а у компонента для которого был комплект малых тестов - да.
Именно это и есть рефакторинг и он может и должен приводить к красным тестам. Если вы работаете с кодовой базой, а не с юнитом.
Да вы действительно правы, что R-G фаза набор экспериментов с кодом, не связанная с рефакторингом. Однако изначально Кент Бек вкладывал в неё свой личный подход и просил не возводить его опыт в догму.
Не для каждого кода нужно получить фазу red в начале. Она была нужна в 2000 году, т.к. у них не было инструментария который позволял работать иначе.
Например сейчас я могу задекларировать в IDE функцию/метод с сигнатурой и выбросить исключение "Not Implemented", нажать комбинацию клавиш и мне сгенерируется тест сьют, нажать пару раз tab и получить тест вызывающий нереализованную функцию.
Это всё ещё TDD или нет? Если нет - добро пожаловать в догматики и ваше дело проиграно. А если да, то вы нарушили свои же утверждения - пропустив ненужную фазу.
Часть кода вообще не имеет смысла тестировать описывая контракт до того как сформирован код. Существует проксируемый код. Например бизнес-сервис который вызывает единственный метод репозитория. Совершенно всё равно в какой последовательности вы это сделаете.
И все эти "исключения" существуют в рамках понимания того, что вы делаете. Если вы знаете какой код вы пишете, можете писать его до теста. Главное войти в цикл eventualy. Но если вы пишете код который не понимаете как должен работать, то в цикл надо входить до него.
Пример для объяснения предыдущего абзаца:
Есть сложная структура данных, её нужно преобразовать в сложный аналитический запрос. По факту, вы пишете планировщик и оптимизиатор запросов. Вы понятия не имеете как это сделать и к каким запросам это приведёт. Но вы понимаете, что на вот эти входные данные получите из базы вот эти выходные данные. Самое время написать тест до кода. Т.к. после того как вы напишите вторую имплементацию - у вас всё сломается в первой или вообще перестанет хоть как-то функционировать.
Иными словами TDD это про итерации в первую очередь, а не про то в какой последовательности они выполняются. Единственное что важно, что перед рефакторингом вы должны оказаться в зоне "GREEN".
Например сейчас я могу задекларировать в IDE функцию/метод с сигнатурой и выбросить исключение "Not Implemented", нажать комбинацию клавиш и мне сгенерируется тест сьют, нажать пару раз tab и получить тест вызывающий нереализованную функцию.
Это всё ещё TDD или нет? Если нет - добро пожаловать в догматики и ваше дело проиграно. А если да, то вы нарушили свои же утверждения - пропустив ненужную фазу.
Очень хорошая иллюстрация и замечание по поводу слепого следования догмам.
Я считаю, что описанный вами подход вполне укладывается в концепцию TDD. Почему? Да потому что тест был написан до начала работы на реализацией. Это означает, что теперь вы не забудете, что этот метод у вас не реализован.
С другой стороны, в данной ситуации не вижу существенной разницы: сначала объявлять метод, а потом писать/генерировать тест, или наоборот. То есть следование догме в данном случае ситуацию никак не ухудшило бы. Более того, если бы вы стачала написали тест, вызывающий несуществующий метод, то вы могли бы на этом и остановиться, считая фазу Red завершенной. Необходимости объявлять метод и кидать в нем исключение уже не было бы. То есть, и тут вы получаете некоторую, пусть и минимальную, но экономию времени.
вызывающий несуществующий метод
а если разработка идет в стеке где нельзя вызвать несуществующий метод? Например C\C++\C#. Я поэтому и отошел от принципа сначала тест, потому как глупо писать метод-заглушку, с кучей warnings о не использованных аргументах.
Существует мнение (я за него не агитирую — просто констатирую факт), что ошибка компиляции — это достаточное условие, чтобы считать этап Red завершенным.
Если же этот подход не годится, тогда вообще все равно, что писать сначала — тест или метод заглушку.
В любом случае, даже если вы будете догматично следовать принципам TDD, будет либо чуть лучше (не потратите время на заглушку), либо так же. Ничего не потеряете.
Поскольку я работаю на стеке, который позволяет так делать, меня вариант с ошибкой вызова несуществующего метода вполне устраивает.
Не для каждого кода нужно получить фазу red в начале. Она была нужна в 2000 году, т.к. у них не было инструментария который позволял работать иначе.
Можно про это подробнее: какого инструментария не было 2000-м году у Бека?
Если вы про это
Например сейчас я могу задекларировать в IDE функцию/метод с сигнатурой и выбросить исключение "Not Implemented", нажать комбинацию клавиш и мне сгенерируется тест сьют, нажать пару раз tab и получить тест вызывающий нереализованную функцию.
то ваш тезис неверен. TDD рождалось на Smalltalk-е.
Реальность: TDD — это способ проектирования.
Ну вот ни разу. Тем более, что вы сами себя опровергли в п10. TDD к проектированию имеет такое же отношение как автомастерская к автопроизводителю. Простой, чистый и понятный код -- это и есть способ проектирования.
Реальность: TDD экономит время на отладке и переосмыслении.
TDD обнаруживает часть ошибок до того, как они будут репортированы клиентом. Тесты не инвестиция, а мусор, кодовая база которого может до 10х раз превышать бизнес код. Инвестиция -- функционал и качественный код, к которому TDD не имеет никакого отношения. Говнокодить на ура можно и по TDD.
Код становится проще, понятнее и устойчивее к изменениям. Возникает меньше багов, проще менять требования.
Вот эта основная херь, на которую ведутся миллионы. Все с точностью до наоборот. Мелкие итеративные шаги ведут к многократному покрытию одного фрагмента. Поэтому минимальное изменение требований функционала валит стопицот тестов, ковырять которые приходится долго и рутинно. Поэтому любой рефакторинг вообще кладется в долгий ящик, т.к. никому не уперлось возиться мегабайтами свалившихся тестов, при всем при том, что это лишь внутреннее архитектурное изменение, которое не трогает заявленный функционал.
Реальность: Маленькие шаги позволяют проектировать поведение.
Для того, чтобы построить небоскреб, нужно сложить кирпичи в стену и проверить ее на прочность. В следующей итерации подумаем что с этим делать. Проектирование поведения происходит в бизнес коде. Тесты -- для фиксирования поведения и проверки корректности (соответствии поведения заявленному контракту). А уж никак не для проектирования.
Вывод: пишите функциональные тесты, которые проверяют внешнее поведение (API, спецификацию, т.д.). Избегайте многократного покрытия. Проектируйте без TDD.
Понимаю ваши аргументы, но тут вопрос именно в терминах. Когда я пишу «TDD — это способ проектирования», речь не про «архитектурное проектирование» на уровне системы, а про итеративное проектирование поведения кода. Маленькие шаги в TDD — это не про «построить небоскреб кирпичами», а про уточнение интерфейсов и контрактов по мере их написания.
Что до «мусорных тестов» — да, при механическом следовании «пиши тесты вперед» легко получить горы ненужного кода. Но это не цель и не сущность TDD. Суть в том, чтобы тесты были инструментом мышления и проверки гипотез. Если тесты не помогают, а мешают — это уже не TDD, а просто неудачная реализация практики.
Поэтому возражение «проектируйте без TDD» работает только если понимать TDD исключительно как «наклепать кучу мелких юнит-тестов». Но в изначальном смысле TDD — это как раз дисциплина, которая помогает проектировать код так, чтобы он был удобен для изменения и расширения.
Да, я понимаю. Но в этом и состоит основная проблема. Сначала вы пишите мелкие компоненты, тестируете каждый из них. Затем тестируете из связку. Затем добираетесь до функционального уровня, например API и тестируете уже его. В итоге функционал одного и того же компонента покрывается многократно, что затрудняет будущий рефакторинг и вообще любые изменения. При всем при том, что ваш внутренний дизайн никому особо не упал, важно, чтобы был соблюдён контракт на уровне API.
нужно сложить кирпичи в стену
вот как раз юнит-тест и гарантирует что ваш кирпич правильной формы, и он гарантировано встанет ровно и надежно вместе с другими кирпичами.
А зачем покрывать один и тот же функционал в разных тестах? Честно говоря не пойму для чего. Для меня, функциональное тестирование, бывает нужным, если в связке есть либа\система которая не вызывает доверия. А если все модули уже надежны, то функциональное тестирование это так сказать "шлифовка". Но предполагаю, что это все зависит от области применения.
Пирамида тестирования утверждает, что каждая единица поведения должна быть проверена ровно 1 раз и на том самом нижнем уровне, на котором её можно проверить. Не все можно проверить на самом нижнем уровне – уровне модульных тестов без интеграций. Но если уж вы проверили что-то на более низком уровне, то на более высоком дублирующих тестов быть не должно, это же очевидно.
Как результат, ситуация "функционал одного и того же компонента покрывается многократно" невозможна. Более того, если тесты пишутся именно на поведение, а не на детали внутренней реализации, то проблем с рефакторингом не будет. Почему, очень хорошо объяснено в книге Хорикова про модульное тестирование.
Пирамида тестирования не указывает, что такое единица поведения и где провести адекватную границу. Некоторые следуют правилу один класс -- один тест. Некоторые тестируют геттеры-сеттеры, тогда как другие -- полностью загруженное приложение. Третьи -- делают "срезы" вообще по принципу быстро (юнит) -- медленно (интеграционный) и тупо мокают базу данных. Поэтому если есть возможность выделить и изолировать функциональную единицу как модуль, то здесь и должен проходить уровень тестов.
Если же вы работаете по классическому TDD, то вы как правило пишите тест для каждого из компонентов вне зависимости от функционала и уровня, ибо для тестов уровня модуля будет слишком большая итерация. Отсюда берется многократное покрытие.
Вы правы, подходов много.
Но все же пирамида тестирования утверждает, что тест должен быть реализован на самом нижнем уровне, на котором это возможно. (Иначе пирамиды не выйдет, очевидно же?)
Пример. Валидация введенной суммы перевода.
Можно проверять прям от UI через сервер и до появления красной картинки обратно на UI. Получится сложный, медленный, дорогой, хрупкий e2e-тест, который тестирует поведение.
Но самый ли нижний это из возможных уровней, где проводится валидация?
Нет, конечно!
Мы можем убрать UI и проверять, допустим, что вход в функцию валидации какого-то значения, возвращает или не возвращает ошибку (которая потом бы в UI превратился в красную картинку). Так мы опустили проверки на уровень ниже. Проверяется ли при этом нужное поведение? Да, если к нему добавить 2 интеграционных теста, которые проверят, что значение из UI корректно в функцию записывается, а её возвращаемое значение корректно в UI устанавливается.
Что мы получили? Интеграция проверена 1 раз. Валидация введенного знания - 1 раз. Многократного покрытия нет.
Что касается TDD, то, наверное, Вы правы, что сначала будет написан один высокоуровневый e2e тест. Но ничто не мешает Вам на стадии рефакторинга рефакторить не только код, но и тест (или мешает? вопрос автору статьи, кстати @iv660 подразумевает ли рефакторинг и рефакторинг ранее написанного теста? ), разбивая 1 большой тест на несколько меньших, опуская все возможные проверки на более низкие уровни. Так вы избежите многократного тестирования и получите надежные, дешевые, быстрые тесты, которые не боятся рефакторинга, ибо каждый ваш тест будет соответствовать тому уровню, на котором реализовано соответствующее поведение.
У нас вроде работает \Разведенные в сторону руки/
вопрос автору статьи, кстати @iv660 подразумевает ли рефакторинг и рефакторинг ранее написанного теста? )
Ещё раз подчеркну основную мысль статьи, что целью TDD не является тестовое покрытие. Поэтому созданные в рамках TDD тесты лично я никогда иначе как для исправления найденных в них ошибок не изменяю.
Тест изначально создаётся на минимально возможном уровне, что минимизирует вероятность дублирования.
Для разработки же качественного тестового покрытия можно использовать другие методики, включая упомянутые вами рефакторинг и оптимизацию тестов постфактум.
Если коротко подытожить: дорабатывать тесты можно и полезно, но это не является целью TDD.
Поэтому созданные в рамках TDD тесты лично я никогда иначе как для исправления найденных в них ошибок не изменяю.
Тесты для вас не код? Или у тестов какие-то другие стандарты внутреннего качества? Вы в тестах, например, дублирование не устраняете?
Цитата из моего комментария:
Тест изначально создаётся на минимально возможном уровне, что минимизирует вероятность дублирования.
Нет, дублироавние не устраняю, потому что оно не возникает.
Дело в том, что TDD заведомо подразумевает, что тест покрывает одну конкретную ветку логики (то есть соответствует одному минимальному требованию). У каждого теста свои входные данные и свои проверяемые условия. И каждый проверяемый юнит проектируется так, чтобы он выполнял одну и только одну задачу. Так что рефакторить тут обычно нечего.
Можно, конечно, выносить некоторую общую инициализацию, но на практике она редко оказывается действительно общей: как правило, изменение инициализации под требования одного теста будет ломать остальные.
И тесты и код, реализующий логику приложения, — это, разумеется, не одно и то же. И требования к ним совершенно разные. Например, на тесты не пишутся тесты.
Всё это очень странно.
Во-первых, связь "минимально возможного уровеня" теста и дублирования сама по себе звучит сомнительно и вызывает массу вопросов из серии "что хотел сказать автор". Но это ладно.
Во-вторых, замечу, что "минимизация" вероятности дублирования не тождественна гарантии отсутствия. Это тоже не принципиально.
Но как может инициализация (а так же очистка после) быть не общей? SetUp/TearDown и их наследники же не просто так в xUnit-е поддерживаются, нет? Даже в этой наитривиальнейшей задачке присутствует дублирование в инициализации тестов. И предложенные вами в комментарии тесты будут это дублирование иметь — если его в какой-то момент не устранить.
Вообще, причин для изменения тестов в процессе разработки просто немерено.
Рефакторинг. Как бы там не плясали вокруг проблему "хрупкости" (по-моему, сильно преувеличенной, но это отдельный разговор), но если меняется дизайн, то может поменяться контекст работы компоненты. Следовательно,. как минимум придётся изменить "инициализацию". А на самом деле может много чего поменяться — на уровне контракта этой компоненты. Как тут без изменения тестов? Старые выкидываем, пишем новые с листа?
Ладно, допустим, рефакторинг каким-то чудом мы исключили из причин для изменения тестов. Но как быть с (независящими от разработчика) изменениями "архитектуры", внешних библиотек/фреймворков/сервисов/…чего-угодно-ещё…
А как быть с квинтэссенцией всего это бардака и тем, ради чего всё это (якобы) вообще затевалось — с изменениями требований к системе в целом…
А ещё вы очень лихо спихнули (только не понятно на кого) проблему оптимизации, которая непосредственно затрагивает TDD: количество тестов будет расти, а значит будет увеличиваться время их работы, а значит время отклика (обратной связи) будет расти — рано или поздно захочется их оптимизировать… Как это делать, не изменяя тестов? Или кто-то будет это делать за нас? Напомню: мы всё ещё про разработку, а не про тестирование и покрытие.
Ах, да — чуть не забыл, возможно, самую главную причину для изменения тестов — разработчика. Он меняется! Меняется его понимание того, что он разрабатывает. Соответственно, меняется язык, которым он своё понимание записывает. Тест в TDD — это спецификация требования на примере. И очень важно, не просто знать, проходит тест или ломается, а понимать его суть. А задача сформулировать тест/спецификацию так, так чтобы и самом разработчик в будущем, и друге люди смогли понять смысла теста — далеко не тривиально. Решить её с первого захода "может не только лишь каждый". У вас это стабильно получается?
Ещё, конечно, можно было бы сослаться на авторитетов, но не буду. Если захотите, найдёте цитаты того же Бека про то, что код тестов — такой же код, как и любой другой, и точно так же требует поддержания чистоты, простоты и ясности.
Если честно, я вообще не могу представить, как вам удаётся не менять тесты?… Без обид (я спрашиваю для понимания, не с целью победить в полемике, честно!), но вы действительно практикуете TDD в "промышленной" (или как то назвать лучше) разработке? Давно?
Спасибо за такой развернутый комментарий, приятно видеть ваш интерес к поднятой мной теме.
По существу поднятых вами вопросов:
В первую очередь отмечу, что я являюсь убежденным противником слепого следования догмам. Я не ищу универсальных рецептов на все случаи жизни, а предпочитаю применять инструменты, наиболее пригодные в каждой конкретной ситуации.
Все сказанное мной не является какими-то догмами. Я лишь озвучиваю выводы, сделанные мной по ситуациям из личной практики (ну, и в какой-то мере подсмотренные у других), в которых применение тех или иных методик достаточно часто оказывалось эффективным.
Во-первых, связь "минимально возможного уровеня" теста и дублирования сама по себе звучит сомнительно и вызывает массу вопросов из серии "что хотел сказать автор"
Я имел в виду, что тест всегда тестирует одну единственную ветку логики. Она как правило предполагает более-менее уникальный набор входных и выходных данных, поэтому и инициализация по большей части уникальна, а дублирование — минимально. Настолько минимально, что в большинстве случаев от его устранения бывает целесообразнее отказаться.
С другой стороны, это не догма, и, если это оправданно, никто не запрещает рефакторить и тесты. Но я хочу особо подчеркнуть, что рефакторинг я не считаю изменением тестов, поскольку рефакторинг по определению не предполагает изменения внешнего поведения системы.
Но как может инициализация (а так же очистка после) быть не общей? SetUp/TearDown и их наследники же не просто так в xUnit-е поддерживаются, нет?
Выше я уже упомянул, что необходимость в общей инициализации в моей личной практике возникает редко. Тут я лишь добавлю, что настолько редко, что в моих проектах set-up и tear-down практически никогда не встречаются.
Рефакторинг. Как бы там не плясали вокруг проблему "хрупкости" (по-моему, сильно преувеличенной, но это отдельный разговор), но если меняется дизайн, то может поменяться контекст работы компоненты. Следовательно,. как минимум придётся изменить "инициализацию".
Про рефакторинг писал выше. Если у нас меняются требования, это уже по определению не рефакторинг.
А на самом деле может много чего поменяться — на уровне контракта этой компоненты. Как тут без изменения тестов? Старые выкидываем, пишем новые с листа?..
А как быть с квинтэссенцией всего это бардака и тем, ради чего всё это (якобы) вообще затевалось — с изменениями требований к системе в целом…
Да, именно так. Поскольку в рамках TDD тесты являются отражением требований, то когда какие-то требования устаревают, мы просто удаляем соответствующие тесты. Если добавляются новые требования, запускаем новый цикл разработки Red-Green-Refactor, в результате которого у нас появляются новые тесты.
А ещё вы очень лихо спихнули (только не понятно на кого) проблему оптимизации, которая непосредственно затрагивает TDD
Оптимизация — это одна из трех причин изменения кода наряду с рефакторингом и изменением функционала. Оптимизация не предполагает ни изменения требований, ни применения TDD.
При оптимизации мы хотим, чтобы поведение системы оставалось таким же, но изменялись какие-то ее нефункциональные характеристики, например быстродействие. Поэтому набор тестов при этом не меняется. Отсюда два следствия: (1) оптимизация не дает оснований для применения TDD и (2) мы можем использовать тесты, полученные впроцессе разработки, чтобы сделать процесс оптимизации контролируемым, убеждаясь, что оптимизация не нарушает функционал системы.
Ах, да — чуть не забыл, возможно, самую главную причину для изменения тестов — разработчика. Он меняется! Меняется его понимание того, что он разрабатывает.
Очень точно подмечено. Мы меняемся, и меняется наше понимание, в том числе, и требований к системе. И вот тут пора вспомнить о догмах.
Тесты не нужно менять только в том случае, когда они соответствуют нашему пониманию требований к системе. Если изменилось наше понимание требований, считаем, что изменились сами эти требования. И тогда возвращаемся к описанной выше процедуре: тесты по неактуальным требованиям удаляем, на привнесенные (в следствие нашего переосмысления) — запускаем новый цикл RGR.
Если честно, я вообще не могу представить, как вам удаётся не менять тесты?
Совсем не менять — не удается. Просто это надобится очень редко. Примеры случаев, когда надобится, я как раз и описал в этом ответе.
вы действительно практикуете TDD в "промышленной" (или как то назвать лучше) разработке? Давно?
Да, практикую. Около 4 лет. Поначалу у меня были сомнения в эффективности этого подхода, поскольку в отсутствии опыта разработка действительно ощутимо замедляется. Но это быстро проходит, и в среднесрочной перспективе с TDD получается несколько быстрее, чем без него.
За это время я довольно хорошо изучил эту методику сам и познакомил с ней коллег. Некоторых даже смог заразить идеей.
Как говорил лектор на одном из семинаров по этой теме (не помню его имени и не могу поручиться за дословность цитаты, за что прошу меня извинить), те, кто попробовал и понял TDD, обратно уже не возвращаются.
Пример. Валидация введенной суммы перевода.
Это хрестоматийный пример, наряду с калькулятором. Валидация -- хорошо изолируемая функциональная единица, которая полностью инкапсулирует всю логику. Да, юнит тесты позволят сделать более полное покрытие, нежели e2e. Проблемы начинаются, когда логика компонента содержит внешние зависимости, поведение которых придется мокать. Например, в случае моканья базы данных, зачастую часть логики бывает выполнена в компоненте, а другая в самом SQL, и оба представляют собой единую функциональную единицу. Поэтому такой тест с моком базы становится бесполезным.
Для того, чтобы при тестировании связки не закладываться на функциональность каждого мелкого компонента, вроде бы тыщу лет назад и придумали интерфейсы и DI?
В результате функционал одного и того же компонента не покрывается многократно, потому что в тестах связки участвует не сам этот объект, а его мок.
Это все сферический конь в ваккууме. Как это бывает реально:
Вы тестируете компонент. DI вам говорит, что компонент завязан на entity репозиторий базы данных. И вы здраво хотите его мокнуть. В репозитории стопицот queries. Нужно ли их всех мокать? Нет. В итоге вы судорожно пытаетесь найти какие из них реально используются компонентом, чтобы отмокать только их. Потом замечаете, что некоторые findByXXX()
репозитория параметризированы, и пытаетесь разобраться в SQL логике, чтобы точно определить что же делает база. Затем в тесте пытаетесь симулировать какой-то сценарий. В итоге получается 'given' простыня из предусловий и моков, понятная только вам, здесь и сейчас. Ни один читающий после вас эту лабуду не поймет на лету почему findByXXX()
должен возвратить именно 42. После многочисленных попыток отдебажить сценарий и установить правильное поведение моков, вы, наконец, получаете зеленый тест, запускате приложение и... бум! ничего не работает. Ошиблись в моке, не учли поведние бд, ретроградный меркурий, еще что-нибудь.
На следующей неделе компоненту вместо findByXXX()
нужно использовать более продвинутыйfindByYYY()
. Одна строчка изменений в бизнес коде и двести рутинных поправок в тестовых моках.
Уже давно моки практически не использую. Вместо этого стараюсь проектировать систему так, чтобы бизнес-логика никогда не лазила за данными в базу. Вместо этого все необходимые данные передаются в качестве аргументов.
В итоге получается 'given' простыня из предусловий и моков, понятная только вам, здесь и сейчас.
Вы очень наглядно показали, что в описанной ситуации надо использовать не моки, а другие виды Test Doubles.
Только такая ситуация — как вы опять же очень точно подметили — возникает не при TDD (разработке), а при тестировании.
При правильном же использовании TDD как инструмента проектирования ситуация типа
вы судорожно пытаетесь найти какие из них реально используются компонентом
исключена, так как вы этот самый компонент разрабатываете и знаете, какие именно методы репозитория вам нужны.
Если же логика работы с репозиторием в какой-то момент становится сложной — это явный повод выделить объект, который будет позволять абстрагироваться от этой сложности. Рефакторинг (в том числе и тестов!) в руки.
Впрочем, TDD как инструмент проектирования вы же отвергаете… Может, в этом и проблема?
Вы сами очень точно сказали:
Тесты -- для фиксирования поведения и проверки корректности (соответствии поведения заявленному контракту).
Я бы уточнил: для фиксирования требований и проверки соответствия этим требованиям. Когда вы сначала формулируете требования, а только потом проектируете, — у вас есть все шансы этим требованиям соответствовать. В этом и состоит суть TDD.
Мне вот любопытно. А сколько вы проработали на одном месте, где потворствовали идее писать функциональные тесты. Лет 5-10 удалось хотя бы?
Вот прямо на месте написания кода, а не повышения в тимлиды, архитекоры, руководители групп и так далее?
Текст вашего комментария просто в каждом пункте демонстрирует не понимание проблем которые таким образом создаются.
Простой, чистый и понятный код -- это и есть способ проектирования.
Не существует простого и чистого и понятного кода.
Код бывает или примитивный - т.е. многословный и монотонный, что приводит к пропуску ошибок - т.е. не бывает понятным (только создаёт такое впечатление). Либо бывает с когнитивной нагрузкой - библиотеками и/или DSL. Что сразу перестаёт быть "простым", зато становится понятным.
Чистый код, это сотни итераций рефакторинга, которые и создают в итоге элегантность и чистоту. Но хотелось бы посмотреть как вы отрефакторите какой-то компонент и воспользуетесь "функциональными тестами" и хотелось бы глянуть, сколько времени это займёт.
TDD обнаруживает часть ошибок до того, как они будут репортированы клиентом.
Тесты не обнаруживают ошибки. Тесты фиксируют поведение и ожидания - создают контракт. Они документируют то, что ожидается от кода. Это и есть контракт вашего компонента, а не "соблюдение API".
Потому что если вам репортирует клиент - он может уже быть мертвым в некоторых ситуациях. А в других - его бизнес будет мертв.
Поэтому любой рефакторинг вообще кладется в долгий ящик, т.к. никому не уперлось возиться мегабайтами свалившихся тестов, при всем при том, что это лишь внутреннее архитектурное изменение, которое не трогает заявленный функционал.
Я зарабатываю на рефакторинге. Каждый раз когда мне нужно что-то отрефакторить я беру это и делаю. А вот проекты у которых 0% покрытия и 5 лет разработки, после которой кодер стал архитектором - в них вот никогда ничего не рефакторят.
т.к. никому не уперлось возиться мегабайтами свалившихся тестов, при всем при том, что это лишь внутреннее архитектурное изменение, которое не трогает заявленный функционал
Всё верно. Именно поэтому существует Open Close Principe. Который говорит - создай новое поведение, а не меняй старое. Если комонент перестает работать в рамках контракта тест упадёт, т.к. функционал компонента - изменился. Создайте новый компонент и выведите старый из эксплуатации.
Изменение же поведения 1 компонента не приводит к развалу всего стека, только самого компонента и i9n теста. Ну. Если вы умеете конечно программировать и писать чистый код, о котором упоминали. Тот у которого всегда - Single Responsibility. Благо это не сложно, для тех кто в продуктовых командах то работает.
Не существует простого и чистого и понятного кода.
Если ваша команда не может генерировать простой и понятный код, или же вы используете, средства разработки, которые не способствуют простой и понятной огранизации кода и помимо бизнес логики привносят стороннюю когнитивную нагрузку, то боюсь, никакое TDD вам здесь не поможет. Весь ваш проект с самого начала -- это мусор.
Чистый код, это сотни итераций рефакторинга, которые и создают в итоге элегантность и чистоту.
Как правило за рефакторинг никто вам платить не собирается. Продакт всегда требует новых фич. Если команда изначально создает технический долг, в надежде, когда придут хорошие времена и можно будет его спокойно отрефакторить, то они ошибаются -- такие времена никогда не настанут. Проще будет переписать весь продукт с нуля.
Они документируют то, что ожидается от кода. Это и есть контракт вашего компонента,
Полная херь (не в обиду вам). Документирует документация. Контракт определяется дизайном, и такими средствами как WSDL, OpenAPI, UML, etc. А вот тесты могут проверить соответвтвие имплементации дизайну. Ибо никому в здравом уме не впилось разбираться с вашими whenSomeShitHappend_thenShouldReturnXXX()
, написанными на кривом языке с кучей непонятных предусловий. Есть конечно BDD и жесткая спека, когда вы пишите софт для NASA, но это, думаю, не ваш случай.
Потому что если вам репортирует клиент - он может уже быть мертвым в некоторых ситуациях. А в других - его бизнес будет мертв.
Тем не менее, все конторы тестируют на хомячках. А по поводу бизнеса и к великому сожалению даже жизни, поймите одно простое правило -- всем пофиг. Защита бизнеса клиента -- дело рук самого клиента (SLA, fallback, etc.). В большинстве же случаев клиентский сектор уже настолько приспособился к случайным сбям и ошибкам, что это перестало быть проблемой.
А вот проекты у которых 0% покрытия и 5 лет разработки, после которой кодер стал архитектором - в них вот никогда ничего не рефакторят.
Не совсем понимаю почему 0% покрытия? Я говорил о функциональных тестах, которые тестируют лишь заявленный функционал, а не внутреннюю кухню. Например, API тесты микросервиса. Для последующего рефакторинга это как раз идеально -- вы смело можете хоть поменять базу данных, хоть перелопатить весь внутренний код, не боясь, что это затронет консьюмеров.
Изменение же поведения 1 компонента не приводит к развалу всего стека, только самого компонента
Это наивное идеализированное представление о реальной разработке. Уверяю вас, что уже на следующей итерации вы получите новое бизнес-требование, причем тривиальное, которое как серпом по яйцам пройдется по всей вашей стройной архитектуре, требуя перелопатить большинство контрактов, начиная от API и кончая базой. И вместе с этим еще стопицот бесполезных тестов.
Для конкретизации п.5 вот даже целую детальную статейку набросали, почему так выходит...
Спасибо за ссылку — хороший кейс в поддержку п.5. Статья говорит про модульные тесты (не про TDD напрямую), но выводы те же: ранняя фиксация требований и быстрый фидбек сокращают общий цикл.
TDD хороша уже тем, что она есть.
Поясню. Если нашлась ошибка, то я для её фиксации пишу тест, который, конечно же, поначалу падает. Ну, правлю код так, чтобы не падал, это очевидно. Но если архитектура построена так, что там нет места тестам, то получается сплошная боль - тестирование только вручную, а это медленно и не гарантирует, что следующая правка кода не разрушит функциональность, ранее протестированную вручную.
приходится писать тесты, продумывать интерфейсы, проводить рефакторинг. Но эти усилия окупаются уже в среднесрочной перспективе. Код становится проще, понятнее и устойчивее к изменениям
Как это окупается по сравнению с подходом когда вы сначала реализуете, а потом фиксируете поведение тестом? Ведь пока вы над реализацией работать не закончили, вы будете переделывать свои тесты множество раз, никогда не видел чтобы было по другому.
Попробую ответить за автора статьи.
Потому что, когда после написания приложения озаботитесь тестами, архитектура приложения будет такой, что просто так тестов в неё уже не встроить. Многочисленные зависимости от других модулей, требующие моков, зависимость от железа, не предусматривающая режима симуляции - это только то, что пришло в голову за минуту.
Я глубоко убежден что если архитектуру нужно подстраивать под тесты — то нужно выбрасывать к чертям такие тесты
Моё мнение, что если архитектура не позволяет встраивать в себя тесты без танцев с бубном (обилие моков и DI), то надо пересматривать архитектуру (как не отвечающую принципам SOLID).
Если тестам нужна какая-то специально спроектированная под них архитектура, значит тесты на завязаны на детали реализации и тестируют не то что нужно
Представьте, что вы специалист QC на заводе электроники, и вам приносят на проверку залитую компаундом плату без тестовых пинов, и говорят: тестируй. Вы говорите, что по ISO номер ХХХ такие платы надо тестировать через JTAG, а вам в ответ: выбрасывай к чертям свои тесты и свой ISO и изобретай новые, мы ничего в архитектуре платы менять не будем. Норм тема, а?
Это называется по-другому - я что-то накодил такое, что нормально нельзя покрыть юнит-тестами, но переделывать не буду, т.к. тесты отстой, а я гений!
На самом деле последовательность изначально была другая. Вначале шли требования к продукту. Если вы изначально не зафиксировали, что вы хотите получить, то вы получите "что-то", и оно будет пахнуть. Требования -- это не всякое утвердление, а недвусмысленное, понятное, реализуемое, и, главное, тестируемое.
Если есть требования перед началом работы -- можете кодировать без тестов, они появятся после. Ведь ваши требования, как я писал выше, тестируемые, то есть вы представляете, как вы будете проверять соответствие продукта требованиям? Значит и архитектуру менять не придется.
С появлением разных сортов эджайла requirement books ушли в прошлое. Сама необходимость никуда не отпала, но по факту этого стали избегать (хотя эджайл как раз настаивает на наличии требований, даже при том, что они будут меняться). В итоге, поскольку необходимость тестирования все-таки никто не отрицал, требования решили заменить тестами, но писать их заранее. А отсюда уже и влияние их на архитектуру.
Ну а если вам по душе ничем не ограниченная свобода действий кодировать то, что пришло в голову только с утра, и так каждый день... то выбрасывать, скорее всего, придется не тесты, а продукт.
Тесты — не самоцель (каковую мысль я также пытался донести в своей статье).
Если у вас возникают проблемы со встраиванием тестов, это индикатор того, что с архитектурой не все в порядке. Если у вас возникают проблемы со встраиванием тестов, то у вас возникнут проблемы и со встраиванием нового функционала, и изменением имеющегося, и с рефакторингом.
Я глубоко убежден что если архитектуру нужно подстраивать под тесты — то нужно выбрасывать к чертям такие тесты
Разбитую на слои архитектуру априори легко тестировать. Самим же потом удобно будет делать доработки.
Если у вас связанный код не покрытый тестами, то любые изменения в коде могут тянуть за собой проблемы. Самый прикол что они вылазят в пограничных состояниях, которые как раз легко тестировать unit тестами.
Про переделку тестов.
Да, иногда их надо переделывать. Зависимости всякие менять, но это нормально.
Мне в основном приходится работать со всякими моделями. И, если модель меняется, то меняются и выходные значения. Вот тут тесты просто спасение - можно посмотреть где рушится и на сколько изменилось значение. Требовалось получить 10 см, а модель выдала 10 нм - тут точно что-то пошло не так. Ну, а если вместо 10 см выдала 10.1 см, то тоже стоит проверить модель на вшивость, хотя и без особого фанатизма.
На практике тесты приходится менять крайне редко. Если вы пишете тесты так, чтобы они отражали требования к системе, и в процессе работы над реализацией вам захотелось изменить тест, задумайтесь - а действительно ли дело в тесте? Чаще всего оказывается, что вы неправильно пишете реализацию.
Просто разработка это итеративный процесс. Как в точности компоненты должны взаимодействовать между собой в полной мере становится понятно только по ходу разработки. Потому что по ходу дела выясняются новые нюансы, уточняются требования и т.д.
Бегите из такой разработки. Это хаос и отсутствие системного подхода.
Да, именно поэтому взаимодействие между компонентами удобно организовывать через ограниченное количество точек взаимодействия (A. K. A. интерфейсов).
Если требования меняются в ходе разработки, мы скорее добавляем в компоненты новые интерфейсы, а не меняем старые. Те интерфейсы, реализации и, соответственно, тесты, которые теряют актуальность, просто удаляем.
Если требования меняются в ходе разработки, мы скорее добавляем в компоненты новые интерфейсы, а не меняем старые.
А почему бы эти интерфейсы не поменять, пока никакие другие модули, кроме тех, которые разрабатываете персонально вы, пока от них не зависят? Зачем так стого держаться первоначального выбора, если вы в нём ошиблись? Нелогично IMHO.
Те интерфейсы, реализации и, соответственно, тесты, которые теряют актуальность, просто удаляем.
То есть, работаем на мусорную корзину? Думаю, начальники будут в восторге! (на самом деле, нет)
Я имел в виду под интерфейсами очень мелкие единицы — например, отдельный метод публичного API. Они изолированы и покрывают минимальное число требований. Поэтому менять контракт чаще всего просто не имеет смысла: проще и безопаснее заменить его новым, а старый удалить, когда он теряет актуальность.
Ключевой момент в том, что интерфейс отделен от реализации. Добавление нового метода не требует больших усилий и обычно не ломает существующую систему. В реальных проектах редко бывает так, что и реализацию интерфейса, и клиентский код пишет один и тот же разработчик. Поэтому «пока от него никто не зависит» — ситуация скорее исключительная.
Конечно, «чаще всего» не значит «всегда». Если изменения действительно минимальны и есть стопроцентная уверенность, что интерфейс еще нигде не используется (или что все места легко скорректировать), то изменить его допустимо. Но в общем случае добавление нового метода куда безопаснее, чем переписывание старого — именно поэтому я и делаю на это акцент.
del
Сложное какое-то объяснение... Лучше на практике въехать: когда попадаешь в ситуацию, что без тестов требуемый функционал ну никак не вырисовывается (пишешь, запускаешь, 100500 действий, чтобы попробовать – нет, не то, и так раз за разом) – очень быстро соображаешь и про tests first, и про то, как их лучше писать.
Я бы добавил еще один пункт: код ревью. Код ревью пересекается с другими пунктами отмеченными автором. Я например делаю код ревью по тестам на сам код. Нет отчета ТДД с покрытием изменений, код не пройдет.
Код-ревью — тоже важная штука.
Я не рассматривал его в статье по той причине, что он работает на более крупном уровне, чем TDD.
Об этом я также упоминал в ответе на комментарий по поводу Agile.
Тесты здесь выполняют роль спецификаций, а не проверки.
Не кажется ли вам, что тесты - это слишком громоздкий и трудоемкий способ писать спецификации, по сравнению с другими способами писать спецификации? Мне - кажется. И главное - что это ещё и слишком трудоемкий способ править спецификации, при том, что при написании нетривиальной программы, в которой сразу не очевидно разбиение на модули и определение функций и зон ответственности этих модулей, спецификации нередко приходится корректировать в процессе разработки.
Другое дело, когда архитектура пограммы в целом уже ясна, и нужно не ошибиться в многочисленных деталях реализации (типа правильности перемещения данных из JSON в БД и обратно, с надлежащими проверками) : там IMHO да, специификации в виде теста уместна - если вообще принято решение тесты писать (трудоемкие они).
Нет, не кажется. Это лишь частично спецификация. Второй частью – это автоматизация проверки соответствия результата этой спецификации. Т.е. заменять тесты только спецификацией – в корне неверно. Вы еще обязаны для полного замещения организовать автоматизацию на другой тип написания спецификации. Если другие типы написания спецификации и будут проще, то вот их автоматизация – навряд ли. Соответственно и сумма спецификации в другом типе и её автоматизации выйдет дороже, чем тесты.
Проблема изменения тестов при изменении внутренней реализации давно решена: тестируются не детали внутренней реализации, а единица поведения. Подробнее в книге Хорикова про модульное тестирование.
Второй частью – это автоматизация проверки соответствия результата этой спецификации. Т.е. заменять тесты только спецификацией – в корне неверно.
А может, лучше "суп отдельно, а мухи отдельно" - в смысле спецификация отдельно, а проверка ее отдельно? Потому что объективно нужны эти вещи в разные моменты времени: спецификация - в процессе создания программы, а тесты - уже на этапе контроля, когда программа создана. И потому что в процессе написания программы спецификации ее частей подвержены изменениям. А потому для работы со спецификаций лучше иметь что-нибудь более легковесное.
А использование тестов в качестве спецификации имеет тот недостаток, что тесты - это код. Код по сравнению с текстом слишком многословен, то есть имеет больший объем, причем объем кода тестов (в строках и знаках) обычно в разы больше объема покрываемого ими кода приложения (правда сложность - цикломатическая и т.п. - кода тестов обычно меньше). В коде, опять же, надо уделять внимание соблюдению церимониалов используемых языков/библиотек/фреймворков (boilerplate),. Код, даже "самодокументируемый", труднее читать, чем текстовое описание, наконец.
Вы еще обязаны для полного замещения организовать автоматизацию на другой тип написания спецификации.
А она точно нужна, эта автоматизация? А то вот мне тут сразу вспоминается акроним YAGNI. Или - Леонов в роли Уэфа, с фразой "А Скрипач не нужен, родной." Потому что спецификация склонна меняться в течение процесса первичной разработки, и чем менее программа тривиальна, тем эти изменения неизбежнее. А проверка реально нужна после завершения этапа первичной разработки, грубо говоря - один раз (не, я в курсе про регрессионное тестирование, но оно будет не на этом этапе, а потом). Так что часто имеет смысл сэкономить на сложности работы со спецификацией в процессе разработки, а не на завершающем этапе. А один раз тесты можно и вручную написать. Или, если спецификация сделана на чем-то распространенном, типа OpenAPI AKA Swagger - использовать кем-то уже сделанные средства автоматического написания тестов по спецификации. Ну, а в наше время - использовать суррогат таких средств на базе LLM: говорят (я про это пока ничего сказать не могу, ибо на грабли с тестами от LLM самолично не понаступал), что с написанием тестов LLM справляется прилично.
Проблема изменения тестов при изменении внутренней реализации давно решена: тестируются не детали внутренней реализации, а единица поведения. Подробнее в книге Хорикова про модульное тестирование.
Мне даже нравится чем-то этот ваш оптимизм - что проблема решена. Решена-то она на бумаге на бумаге. А забыли про овраги: в процессе начальной разработки - всего приложения или какой-то отноительно самостоятельной его функции, проводимом без предварительного детального проектирования (то есть - в обычном практическом случае) меняться может многое, почти всё. В том числе и поведение, и разбиение его на единицы. И уж всяко - интерфейсы, через которые взаимодействуют модули. И чем менее тривильна разрабатываемая программа, тем чаще такие изменения происходят.
PS А вот когда реализация программы тривиальна (типа какого-нибудь проверить какой-нибудь очередной JSON отправить его на обработку, получить результат и вернуть его) и думать не надо, а что делать понятно заранее - там можно и с TDD заморочиться. Другой вопрос, правда - а оно вам хочется заниматься такой работой? И не пугает ли вас при этом перспектива замены на AI?
А может, лучше "суп отдельно, а мухи отдельно" - в смысле спецификация отдельно, а проверка ее отдельно? Потому что объективно нужны эти вещи в разные моменты времени: спецификация - в процессе создания программы, а тесты - уже на этапе контроля, когда программа создана.
2 момента:
Тесты нужны ДО создания программы и ВО ВРЕМЯ её создания тоже. Об этом вся статья про TDD, в комментах к которой мы общаемся (например, пп.1,6,7). Об этом же и Ваш пункт про СПЕЦИФИКАЦИЮ, который мы сейчас обсуждаем. СПЕЦИФИКАЦИЯ подразумевает, что она должна быть написана ДО. Значит, и тесты должны быть написаны ДО.
Также Вы выше пишите, что тесты нужны только на стадии ФИНАЛЬНОГО контроля. Но вот теория менеджмента подразумевает, что контроль бывает не только финальный. Он еще бывает входящий (ДО начала выполнения задачи, т.е.разработки программы), промежуточный и переодический (последние 2 - ВО ВРЕМЯ разработки программы).
Отсюда снова следует, что тесты должны быть созданы как минимум ДО начала работы программы, если мы их хотим использовать для входного контроля, или ВО ВРЕМЯ, если хотим использовать промежуточный и/или переодический.
Автору в п.4 и отдельным пунктом можно добавить про типы контроля и как TDD помогает про них не забывать...Конечно нужно разделять!
Разделение мух от котлет выглядит так: фиксируем мы требования и поведение через написание теста, а контролируем потом через его исполнение. Вас не устраивает такое разделение?
Код по сравнению с текстом слишком многословен, то есть имеет больший объем, причем объем кода тестов (в строках и знаках) обычно в разы больше объема покрываемого ими кода приложения (правда сложность - цикломатическая и т.п. - кода тестов обычно меньше). В коде, опять же, надо уделять внимание соблюдению церимониалов используемых языков/библиотек/фреймворков (boilerplate),. Код, даже "самодокументируемый", труднее читать, чем текстовое описание, наконец.
Ниже в этой же ветке уже ответили, что это не всегда так. Я лишь добавлю, что перечисленные Вами "недостатки" давно придумано как нивелировать и устранять. Учитывая это я бы даже сказал, что запись спецификации в виде тестов ДОВОЛЬНО ЧАСТО получается даже дешевле и проще, чем обычным человеческим текстом.
Отсюда мы возвращаемся к тезису про более дешевую сумму записи спецификации и её автоматизации и получаем, что автотесты становятся практически самым дешевым и эффективным вариантом, как бы со стороны не казалось иначе. Такой вот парадокс, да.
А она точно нужна, эта автоматизация?
Про это п.4. И мои комментарии выше. Я тоже LLM не использовал для генерации тестов, но прекрасно знаю некоторые своды правил, которые позволяют сделать так, что требования из ТЗ, записанные в виде кода теста, получаются проще, чем в тексте самого ТЗ. Возможно, Вы умеете писать ТЗ круче наших аналитиков (в этот вопрос я с вами в дискуссию вступать не стану).
А забыли про овраги: в процессе начальной разработки - всего приложения или какой-то отноительно самостоятельной его функции, проводимом без предварительного детального проектирования (то есть - в обычном практическом случае) меняться может многое, почти всё. В том числе и поведение, и разбиение его на единицы. И уж всяко - интерфейсы, через которые взаимодействуют модули. И чем менее тривильна разрабатываемая программа, тем чаще такие изменения происходят.
Ну, я же не зря Вам целую книгу порекомендовал, наверное? Вот Вы спорите даже не прочитав, похоже. Собственно, как и всю статью... че уж я про книгу-то сетую...
Пожалуй, на этом я остановлюсь, на дальнейшие Ваши комментарии отвечать не стану. Спасибо Вам за содержательную дискуссию.
Тесты нужны ДО создания программы и ВО ВРЕМЯ её создания тоже. Об этом вся статья про TDD,
Это - если следовать TDD как религиозному учению. По жизни тесты нужны В КОНЦЕ первичной разработки программы или более-менее изолированной ее функции. Дальше, конечно будут изменения, так что тесты лучше оставить ради уверенности, что ничего из ранее сделаннного не было сломано (это AFAIK называется умными словами "регрессионное тестирование").
Но вот теория менеджмента подразумевает, что контроль бывает не только финальный.
Тут совсем недавно статья про гиперконтроль была, с общим смыслом что это плохо. Ибо людям воовбще-то не нравится, когда им не доверяют, да ещё - и за счёт траты их времени. Я, кстати, тоже против гиперконтроля, и в своей практике (а мне приходилось побывать и в роли менеджера (хоть и близко от земли), чисто из соображений "если не я, то кто") тщательно старался его избегать: лучше с подчиненых спросить за их косяки потом, по факту, и только если на кого не действует - таки контролировать в процессе. Благо я тогда отнюдь не в армии служил, и добиваться выполнения задач, не имея для этого достаточно качественного персонала, мне в тот короткий момент не пришлось.
Вас не устраивает такое разделение?
Не устраивает. Я хоть и кодоголик - то есть кайф ловлю от написания сколь-нибудь интересного кода, но не от написания нудных тестов. К тому же мне как-то претит писать код даже не в стол (это мне норм), а в мусорную корзину.
Ниже в этой же ветке уже ответили, что это не всегда так.
Я там ответил в ответ. Короче, этот аргумент пока что не обоснован, а потому не принимается.
Ну, я же не зря Вам целую книгу порекомендовал, наверное? Вот Вы спорите даже не прочитав, похоже. Собственно, как и всю статью... че уж я про книгу-то сетую..
Статью-то я прочитал. Книгу - тоже, может быть, прочитаю, но пока что не вижу необходимости (если чо, я даже Библию не читал, примерно по той же причине). И вы уж извините, но отсылку к книге целиком вместо изложения своими словами доводов для конкретной дискуссии (если нужно нечто из книги для подтверждения и конкретизации мысли - с указанием конкретных цитат по правилам цитирования) я расцениваю как способ уклониться от дискуссии.
Но раз вы решили дискуссию не продолжать, я вас целиком и полностью в этом поддерживаю - жаль, что я это увидел не сразу, а когда первоначальный комментарий уже написал. Так что если вы сочтете нужным ещё раз ответить, то от меня ответа уже не ждите.
Код по сравнению с текстом слишком многословен, то есть имеет больший объем, причем объем кода тестов (в строках и знаках) обычно в разы больше объема покрываемого ими кода приложения (правда сложность - цикломатическая и т.п. - кода тестов обычно меньше).
Давайте сравним.
Спецификация метода sum(a, b)
Описание
Метод sum(a, b)
принимает два целых числа и возвращает их сумму.
Параметры
a
(int): Первое целое число.b
(int): Второе целое число.
Возвращаемое значение
(int): Сумма двух целых чисел
a
иb
.
Предусловия
Параметры
a
иb
должны быть целыми числами.
Постусловия
Метод возвращает целое число, которое является результатом сложения
a
иb
.
Исключения
Если
a
илиb
не являются целыми числами, метод должен выбрасывать исключениеTypeError
.
Тесты (достаточные для разработки этого метода в соответствии с принципами TDD и требованиями спецификации)
Function test_sum_with_two_integers()
result = sum(3, 5)
assert result == 8
Function test_sum_with_non_integer_a()
try
sum(3.5, 5)
expect TypeError
Function test_sum_with_non_integer_b()
try
sum(3, "5")
expect TypeError
простите, но Вы забыли постусловие с переполнением и тест на него.
а еще тесты бы граничными условиями дополнить, чего не написано даже в ТЗ (а должно быть, как программа себя ведет в граничных случаях?). sum(-MAX_INT, -MAX_INT)
- это переполнение или успех?
а в остальном - все отлично!
Я привел упрощённый пример. Он демонстрирует, что не всегда формулировка требований будет короче, чем тест.
Что касается граничных условий, ещё раз подчеркну, что всеобъемлющее тестовое покрытие не является целью TDD. Если вам нужны тесты для дополнительных проверок (то есть такие, которые будут зелёными изначально), вы их пишете, но не в рамках TDD.
Давайте сравним.
Давайте сравним, но не с соломенным чучелом. Совершенно не обязательно расписывать спецификацию столь многословно на языке выходных документов по проекту(вы бы ещё на канцелярите ее написали!). Есть более краткие, и при этом вполне человекочитаемые способы спецификации. Например "трехслэшовые комментарии" в C#. Даже если отказаться от статической типизации, чтобы сохранить логику спецификации, всю эту простыню можно записать куда короче
/// <summary>
/// Возвращает сумму двух операндов при условии, что фактически они имеют тип int
/// </summary>
/// <param name="a">Первый операнд, должен иметь фактический тип int</param>
/// <param name="b">Второй операнд, должен иметь фактический тип int</param>
/// <returns>Сумму a+b фактического типа int</returns>
/// <exception cref="InvalidCastException">Возбуждается, если тип одного операнда - не int</exception>
static dynamic Sum(dynamic a, dynamic b)
// Реализацию с вашего повзоления опускаю.
А вот теперь можно и сравнть.
Если что, основное назначение этих комментариев - именно документирование. В частности, на сайте Microsoft документация по библиотеки времени выполнения и прочим стандартным API .NET генерируется из таких вот комментариев в исходном коде.
PS А если использовать статическую типизацию, то можно сократить много слов в спецификации. И часть тестов, кстати - тоже. Но это уже совсем другая история.
К комментарию выше мне остается только добавить, что далеко не всегда описать требуемое поведение словами будет проще и дешевле, чем оформить это поведение в виде теста.
И снова я про Cynefin Framework. TDD идеально с ним согласуется. Начинаем мы в домене Chaos: фичи нет, и как она должна выглядеть -- мы не знаем. После написания теста мы фактически формулируем требование (с тех пор, как вести Requirements Book стало моветоном, тесты стали единственным адекватным способом сформулировать требования). Теперь мы знаем, чего хотим, но не знаем, достижимо ли это: мы в домене Complex. В процессе первой "грязной" реализации мы фактически изучаем предмет: можно ли реализовать фичу, какие есть сложности, правильно ли сформулировано требование на предыдущем этапе... В конце, когда тест наконец проходит, мы приходим к пониманию и того, чего мы хотим, и того, как это нужно реализовывать. Остается выбросить весь грязный код и оставить только это кровью и потом добытое понимание: поздравляю, мы в домене Complicated. А дальше начинается та чистая инженерия, к которой мы, по идее, должны стремиться: что делать -- ясно, как устроена система вокруг нужного кода -- понятно. Остается только взять и написать хороший код. Такой, чтобы читающие его люди (или мы сами через пару лет) оказались бы в домене Clear: чтобы не было ни двусмысленности, ни костылей.
Но на практике мало кто понимает даже что такое тест...
10. Подмена проектирования тестами
Я правильно понимаю, что речь идёт про описанный Беком "классический" TDD? "Лондонскую школу" (BDD) никоим образом сюда не привязываем?
Вы не знаете TDD