Как стать автором
Поиск
Написать публикацию
Обновить

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

Маленькие шаги - это вроде agile называется, не? Спринты по 2 недели, ценность, ретро, демо для этого и были придуманы, чтобы идти на ощупь в условиях создания чего-то нового, неизвестного и потому непонятного.

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

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

TDD идеально вписывается в этот концепт. Обратная связь моментальная и информативная и ты не движешься дальше вперёд, пока не обработаешь и не примешь во внимание обратную связь.

Вы точно подметили сходство.

Agile и TDD — это одна и та же идея, но на разных уровнях: Agile работает на уровне продукта и команды, а TDD — на уровне кода и архитектуры.

Как правильно подметел автор статьи, многие неправильно понимают смысл некоторых вещей. Посмотрите, например, лекцию "Дядюшки Боба", одного из авторов Agile и возвожно поймете суть.

Если не хотите слушать автора, то кратко, "Гибкость" не про процессы в компании, а "гибким" должен быть сам програмист. Прошли те времена когда один пишет код, другой его тестирует, а третий думает архитектуру. Сегодня этого недостаточно. Програмист должен быть силен во всех областях и должен сам проверять свой код перед тем как отправить его в прод. От этого, между прочим может зависить чья-то жизнь. Вот про что 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 о не использованных аргументах.

Реальность: TDD — это способ проектирования.

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

Реальность: TDD экономит время на отладке и переосмыслении.

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

Код становится проще, понятнее и устойчивее к изменениям. Возникает меньше багов, проще менять требования.

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

Реальность: Маленькие шаги позволяют проектировать поведение.

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

Вывод: пишите функциональные тесты, которые проверяют внешнее поведение (API, спецификацию, т.д.). Избегайте многократного покрытия. Проектируйте без TDD.

Понимаю ваши аргументы, но тут вопрос именно в терминах. Когда я пишу «TDD — это способ проектирования», речь не про «архитектурное проектирование» на уровне системы, а про итеративное проектирование поведения кода. Маленькие шаги в TDD — это не про «построить небоскреб кирпичами», а про уточнение интерфейсов и контрактов по мере их написания.

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

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

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

нужно сложить кирпичи в стену

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

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

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

Как результат, ситуация "функционал одного и того же компонента покрывается многократно" невозможна. Более того, если тесты пишутся именно на поведение, а не на детали внутренней реализации, то проблем с рефакторингом не будет. Почему, очень хорошо объяснено в книге Хорикова про модульное тестирование.

Для того, чтобы при тестировании связки не закладываться на функциональность каждого мелкого компонента, вроде бы тыщу лет назад и придумали интерфейсы и DI?

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

Вы сами очень точно сказали:

Тесты -- для фиксирования поведения и проверки корректности (соответствии поведения заявленному контракту).

Я бы уточнил: для фиксирования требований и проверки соответствия этим требованиям. Когда вы сначала формулируете требования, а только потом проектируете, — у вас есть все шансы этим требованиям соответствовать. В этом и состоит суть TDD.

Мне вот любопытно. А сколько вы проработали на одном месте, где потворствовали идее писать функциональные тесты. Лет 5-10 удалось хотя бы?

Вот прямо на месте написания кода, а не повышения в тимлиды, архитекоры, руководители групп и так далее?

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

 Простой, чистый и понятный код -- это и есть способ проектирования.

Не существует простого и чистого и понятного кода.

Код бывает или примитивный - т.е. многословный и монотонный, что приводит к пропуску ошибок - т.е. не бывает понятным (только создаёт такое впечатление). Либо бывает с когнитивной нагрузкой - библиотеками и/или DSL. Что сразу перестаёт быть "простым", зато становится понятным.

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

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

Тесты не обнаруживают ошибки. Тесты фиксируют поведение и ожидания - создают контракт. Они документируют то, что ожидается от кода. Это и есть контракт вашего компонента, а не "соблюдение API".

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

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

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

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

Всё верно. Именно поэтому существует Open Close Principe. Который говорит - создай новое поведение, а не меняй старое. Если комонент перестает работать в рамках контракта тест упадёт, т.к. функционал компонента - изменился. Создайте новый компонент и выведите старый из эксплуатации.

Изменение же поведения 1 компонента не приводит к развалу всего стека, только самого компонента и i9n теста. Ну. Если вы умеете конечно программировать и писать чистый код, о котором упоминали. Тот у которого всегда - Single Responsibility. Благо это не сложно, для тех кто в продуктовых командах то работает.

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

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

TDD хороша уже тем, что она есть.

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

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

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

Попробую ответить за автора статьи.

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

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

Моё мнение, что если архитектура не позволяет встраивать в себя тесты без танцев с бубном (обилие моков и DI), то надо пересматривать архитектуру (как не отвечающую принципам SOLID).

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

А вот с этим утвреждением я полностью согласен.

Но мы говорим не о «специально спроектированной архитектуре», а об архитектуре, допускающей достаточную степень гибкости.

Представьте, что вы специалист QC на заводе электроники, и вам приносят на проверку залитую компаундом плату без тестовых пинов, и говорят: тестируй. Вы говорите, что по ISO номер ХХХ такие платы надо тестировать через JTAG, а вам в ответ: выбрасывай к чертям свои тесты и свой ISO и изобретай новые, мы ничего в архитектуре платы менять не будем. Норм тема, а?

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

На самом деле последовательность изначально была другая. Вначале шли требования к продукту. Если вы изначально не зафиксировали, что вы хотите получить, то вы получите "что-то", и оно будет пахнуть. Требования -- это не всякое утвердление, а недвусмысленное, понятное, реализуемое, и, главное, тестируемое.

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

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

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

Тесты — не самоцель (каковую мысль я также пытался донести в своей статье).

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

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

Разбитую на слои архитектуру априори легко тестировать. Самим же потом удобно будет делать доработки.

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

Про переделку тестов.

Да, иногда их надо переделывать. Зависимости всякие менять, но это нормально.

Мне в основном приходится работать со всякими моделями. И, если модель меняется, то меняются и выходные значения. Вот тут тесты просто спасение - можно посмотреть где рушится и на сколько изменилось значение. Требовалось получить 10 см, а модель выдала 10 нм - тут точно что-то пошло не так. Ну, а если вместо 10 см выдала 10.1 см, то тоже стоит проверить модель на вшивость, хотя и без особого фанатизма.

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

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

Бегите из такой разработки. Это хаос и отсутствие системного подхода.

Да, именно поэтому взаимодействие между компонентами удобно организовывать через ограниченное количество точек взаимодействия (A. K. A. интерфейсов).

Если требования меняются в ходе разработки, мы скорее добавляем в компоненты новые интерфейсы, а не меняем старые. Те интерфейсы, реализации и, соответственно, тесты, которые теряют актуальность, просто удаляем.

Сложное какое-то объяснение... Лучше на практике въехать: когда попадаешь в ситуацию, что без тестов требуемый функционал ну никак не вырисовывается (пишешь, запускаешь, 100500 действий, чтобы попробовать – нет, не то, и так раз за разом) – очень быстро соображаешь и про tests first, и про то, как их лучше писать.

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

Код-ревью — тоже важная штука.

Я не рассматривал его в статье по той причине, что он работает на более крупном уровне, чем TDD.

Об этом я также упоминал в ответе на комментарий по поводу Agile.

Тесты здесь выполняют роль спецификаций, а не проверки.

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

Другое дело, когда архитектура пограммы в целом уже ясна, и нужно не ошибиться в многочисленных деталях реализации (типа правильности перемещения данных из JSON в БД и обратно, с надлежащими проверками) : там IMHO да, специификации в виде теста уместна - если вообще принято решение тесты писать (трудоемкие они).

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

Проблема изменения тестов при изменении внутренней реализации давно решена: тестируются не детали внутренней реализации, а единица поведения. Подробнее в книге Хорикова про модульное тестирование.

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

И снова я про Cynefin Framework. TDD идеально с ним согласуется. Начинаем мы в домене Chaos: фичи нет, и как она должна выглядеть -- мы не знаем. После написания теста мы фактически формулируем требование (с тех пор, как вести Requirements Book стало моветоном, тесты стали единственным адекватным способом сформулировать требования). Теперь мы знаем, чего хотим, но не знаем, достижимо ли это: мы в домене Complex. В процессе первой "грязной" реализации мы фактически изучаем предмет: можно ли реализовать фичу, какие есть сложности, правильно ли сформулировано требование на предыдущем этапе... В конце, когда тест наконец проходит, мы приходим к пониманию и того, чего мы хотим, и того, как это нужно реализовывать. Остается выбросить весь грязный код и оставить только это кровью и потом добытое понимание: поздравляю, мы в домене Complicated. А дальше начинается та чистая инженерия, к которой мы, по идее, должны стремиться: что делать -- ясно, как устроена система вокруг нужного кода -- понятно. Остается только взять и написать хороший код. Такой, чтобы читающие его люди (или мы сами через пару лет) оказались бы в домене Clear: чтобы не было ни двусмысленности, ни костылей.

Но на практике мало кто понимает даже что такое тест...

Отлично сформулировано. Готов подписаться под каждым словом.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации