Pull to refresh

Comments 243

Из инфраструктурного программирования: smoke tests + red green tests.

Smoke-тесты проверяют очевидное (оно запустилось и не обсыпалось). red/green tests - если что-то странно сломалось, перед тем, как чинить, напишите тест, который ловит проблему, а после этого чините.

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

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

Алсо, мне не нравится, когда путают интеграционные и функциональные тесты. Функциональные - проверяют, что оно "реально работает". Интеграционные проверяют, что компонента А подходит к компоненте Б (или ко всей шарашкиной конторе на серверах).

"Ещё, написание red-тестов для сломавшегося обычно адский труд, потому что ломается обычно там, где плохо, а там где плохо, трудно писать хорошо. "

Кажется акцент то в том, что ни изнутри, ни снаружи обнаружить - "там где плохо" почти невозможно. Так как каждый считает себя профи, или как минимум rootGOOD -)

Я не совсем понял о чём речь.

Просто - банально, как везде.

Знать бы где упасть - соломку подстелили бы.

Я далек от убеждения и практика доказывает прямо обратное. Узкие места в 90% случаев появляются там, где никто не предполагает. Это классика. У вас по другому ? вы пишите код и выпускаете в продакшен имея знания о всех узких местах ???

Так я про это и говорю. Если раскладывать солому всюду, где кто-либо когда-либо падал (а ещё лучше перила сделать), то в целом количество "падений" становится меньше.

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

В IaaC основная модель тестирования такая: smoke test'ы, тесты на идемпотентность, интеграционные тесты (которые те же smoke test'ы, но с настоящим дымом), и (если хватит силы воли) функциональные тесты.

Соответственно, из них только функциональные тесты можно писать в red-green режиме, и то редко. Зато можно писать тесты по результатам найденных багов. Если у вас один раз nginx выехал без SSL'я в продакшен (потому что не было наследования группы из-за того, что положение инвентори поменялось), то есть крайне высокий шанс, что когда-то это случится ещё раз. И алгоритм такой: научиться воспроизводить (допустим, простой случай - взять и выкатить), научиться обнаруживать (простейший тест, что SSL есть, т.е. красный тест), исправить (сделать тест зелёным).

Всё.

Вот про этот red-green я и говорил.

Ну, у него одна среда разработки (среда = коллектив). Бывает так, что тесты важнее кода, потому что код будут писать и переписывать, а тесты - то, что держит его в разумных границах. А бывает, что группа профи, которая почти никогда ничего не переписывает, и им тесты нужны только для "corner cases" с обработкой ошибок.

Т.е. первый вопрос для теста: сколько раз тестируемый код будет существенно меняться в будущем? Часто? Редко? Никогда?

Ten or twenty years from now we'll likely have a more universal theory of which tests to write, which tests not to write, and how to tell the difference.

Написал Кент Бек тринадцать лет назад, а воз и ныне там, стало только запутаннее.

UFO just landed and posted this here
UFO just landed and posted this here
>все ли хорошо с этим кодом
Да с этим кодом все может быть весьма плохо, без вопросов. Только вы не сможете его зарефакторить, потому что это вполне вероятно будет не ваш код. А будет это какой-то сложный фреймворк, который написан другими людьми. Он не идеален, он плохо тестируется — но в чем я уверен, так это в том, что ни вы, ни я такой же, но хороший, который легко тестировать, не напишем примерно никогда.

Тут есть конечно исключения, не у всех и не всегда так, но у меня такое бывало много раз. Живой пример — Apache Spark. При всех его недостатках, у него в гитхабе 1703 контрибьютора, а у меня в проекте никогда не было больше 50 — т.е. все мои проекты примерно на порядки меньше спарка. Ну или там возьмите какой-нибудь Node.js. Или браузер любой. Или Postgres, Oracle, ОС свою возьмите, наконец. Вы же не станете писать новый хром, потому что на этом неудобно тестировать, да? Или линукс рефакторить. А именно такие зависимости и вызывают наибольшую головную боль при тестировании.

>Юнит тесты это еще как лакмусовая бумажка для качества кода.
Ну в общем-то да, но с поправкой на то, что это утверждение не универсально.
UFO just landed and posted this here
>Хм… Рефакторить можно только свой код?
Нет. Но есть такой чужой код, на который у вас просто не хватит ресурсов, квалификации и т.п. Если вы его никогда не видели — это не значит, что его не бывает.

Как вариант — работодатель даст вам по шапке, за то что вы делаете не то, за что вам платят. Потому что эффект от рефакторинга линукса вы показать не сможете.
UFO just landed and posted this here
Вы зачем-то игнорируете тот факт, что речь не о рефакторинге (условного) линукса его разработчиками, а о рефакторинге вами (т.е. это разные люди), если вам это потребовалось, чтобы удобнее тестировать линуксное приложение. И тут как раз таится большая разница в трудозатратах и квалификации между средним разработчиком вообще, и средним разработчиком ядра линукса.
UFO just landed and posted this here
>А зачем мне рефакторить ядро линукса?
Речь о ваших/моих зависимостях, которые могут быть сложнее вашего кода на порядки. ОС, база данных, язык и рантайм, крупные фреймворки — все это из таких, которые геморрой при тестировании доставляют, но заменить их чем-то другим или порефакторить, чтобы они стали удобнее — как правило за гранью возможностей (если вы случайно не гугль). Я таких зависимостей за свою практику видел десятки. И при этом имеющее место неудобство тестирования не то чтобы никого не волнует, а просто другие достоинства его перевешивают.
UFO just landed and posted this here
UFO just landed and posted this here
Погодите, а где я говорил что совсем не надо писать? Я говорил, что их бывает неудобно писать, а рефакторить этот код — тоже непрактично. Это не значит, что их вообще не пишут. Ну т.е. иногда проблемы с тестами могут быть проблемами с кодом чужого фреймворка. В этом случае простым рефакторингом кода своего это не всегда решается.

Кстати, реальный пример — жил был когда-то давно J2EE, старых версий. И тестировать в нем было ужасно неудобно. Сначала умные люди придумали спринг, чтобы выделить тестируемый код в такой форме, чтоб тесты стало делать не больно. А потом и авторы уже в JavaEE доперли, что можно сделать и проще — и тоже все упростили. Стало можно тестировать отдельно от контейнера, и сам контейнер стал подниматься приемлемое время.

Но сделать такое же самому — ну где-то на уровне написания своего DI фреймворка. Возможно — но как правило непрактично.
ОС, база данных, язык и рантайм, крупные фреймворки
Что-то из этого можно обернуть в anti-corruption layer при разработке, чтобы поддерживать высокий уровень и консистентность вашего кода. Что-то из этого всегда оборачивается по уже давним индустриальным стандартам, как БД.
Можно. Я просто говорю, что эти вещи и места в коде — именно то, что создает (и часто) геморрой при тестировании, и в тоже время это то, что сложнее всего заменить или отрефакторить. Воообще интеграция — это то, что сложно тестируется, и интегрироваться приходится с тем что есть, а не с тем, что хочется.

А я бы сказал, что легко тестировать код, который без IO. А тяжело тот, который это IO делает или многопоточный. К качеству кода все это отношение не имеет. Даже если код это атомная лапша Гейзенберга, но его можно запустить не показ миллион сервисов и не поднимая базу, то писать тесты легко, но тестов надо будет много. Но все равно их писать легко.

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

Согласен, но не вижу повода для сожаления, такова реальность. В SICP очень классно расписана связь между модульностью и побочными эффектами. Хотите инкапсулировать поведение - получите состояние. Или таскайте везде явно или неявно параметр типа World, что ничего существенно не меняет.

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

либо научите писать код вообще без них

Особенно какие-нибудь базы данных. 8))

А почему именно TDD - это критика ж всего тестирования. А если программировать сложно - то это критика программирования.

А в чем сложность? Просто устанавливаешь состояние до, проверяешь состояние после?

А почему именно TDD — это критика ж всего тестирования.

Ньет. Это критика именно TDD. Тестировать можно всякими разными способами. Можно делать интеграционные/функциональные тесты, можно доказывать корректность, можно просто на какие-то части забить и понимать, что адекватно протестировать это невозможно (например, отсутствие гонок).


Просто устанавливаешь состояние до, проверяешь состояние после?

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

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

Ага, я упомянул:


можно доказывать корректность

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

Да, как-то я упустил эти слова.

>С этим немножко проблема в большинстве языков…
В русском языке. Постановка задачи же на нем, поэтому корректность по отношению к ней хорошо бы. Но нереально.

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

UFO just landed and posted this here

Любое изменение состояния, особенного глобального - статические переменные, запись в файл, сеть, БД. Про связь этого с модульностью я лучше SICP не напишу, лучше там читать.

UFO just landed and posted this here

Тогда у меня есть пара вопросов.

Как замокать API memory mapped file? Есть код, который в много потоков потоков что-то в него пишет, ротирует сегменты и т.п. Хочется понять, что этот код правильно с ним работает.

Как замокать БД, так чтобы в тесте все же проверить, что SQL генерируется правильный синтаксически и что выбираются верные данные? Нужно учесть, что в работе с БД активно используется всякие Postgres специфические upsert, sequence и прочая.

Замокать нужно класс, который исполняет функцию, которой требуется этот API, то есть представить это API как абстракцию, которая имеет больше значения в используемом домене.
Для тестирования этой абстракции нужен интеграционный тест, то есть тот, что с операционной системой. Тем более, абстракция эта скорее всего будет довольно тупа, сложной логики не особо.
Как замокать БД, так чтобы в тесте все же проверить, что SQL генерируется правильный синтаксически и что выбираются верные данные?
Это бесполезная трата времени. БД нужно тоже оборачивать в абстракцию (например, в репозиторий, есть ряд шаблонов вокруг БД), вот эту абстракцию и мокать для клиентов. А SQL не надо проверять при тестировании. Для тестирования абстракции вокруг БД используйте реальную БД, поднятую локально.

Другими словами - все что делает IO нужно спрятать за API и замокать. Само АПИ тестировать с живой ОС, БД и т.д.

Тогда имеем следствия

  • из репозитория нельзя выставлять IQueryable, а это мега удобно.

  • Каждый клиент теперь зависит от мока. Значит как только в репо изменится сигнатура какого-то метода получим некоторый кайф.

    Там где у меня будет extension method для DbContext у вас будет репо, интерфейс, DI и геморой с моками. Но самое главное - я буду использовать IQueryable и это сэкономит мне тысячи строк кода. А вы нет, потому что IQueryable замокать так себе удовольствие.

Другими словами — все что делает IO нужно спрятать за API и замокать
Конечно, и это к TDD не имеет никакого отношения. Это банально вопрос архитектуры кода, вопрос старый, и довольно консенсусный. Варианты архитектур бывают разные, но все примерно об одном же, о разделении ответственностей и расслоении приложения. Как пример, гексагональная архиктура, clean architecture. Если я правильно понимаю, вы из стэка C# — на MSDN тоже подобный подход продвигается, т.к. часть статей про архитектуру из своего обучения я припоминаю оттуда.
из репозитория нельзя выставлять IQueryable, а это мега удобно.
Не знаю специфики вашей платформы. Если я правильно понимаю, это что-то вроде Query Builder. Да, это должно быть отделено от бизнес-логики.
Я понимаю удобство использование всего и везде (это касается вовсе не только DB Query: всегда проще и быстрее прям рядом с обработкой данных написать вызов куда надо и не париться, как разделение кода организовать), но 80% жизненного цикла кода — это не его написание, а поддержка.
Каждый клиент теперь зависит от мока. Значит как только в репо изменится сигнатура какого-то метода получим некоторый кайф.
У вас же статический ЯП? То есть даже тесты смотреть не нужно. А так падают тесты, вы их правите и всё хорошо. Если мы говорим про моки, то это юнит-тесты (изолированные), то есть они запускаются и падают быстро и их можно запускать на локальной машине все сразу. После этого можно их исправлять.
у вас будет репо, интерфейс, DI и геморой с моками
Да, и к TDD это не имеет никакого отношения. Вопрос поддерживаемости кода, maintainability. Аргументы примерно такие же, как почему не стоит говнокодить.
Геморроя нет, т.к. я знаю, почему что делается. Мне так и так придётся воссоздавать среду для тестирования поведения юнита или подиерархии, и делать это фикстурами и более диковинными эмуляциями (среда — это не только РСУБД) намного сложнее.
я буду использовать IQueryable и это сэкономит мне тысячи строк кода

1) Я использую ЯП, в котором литералы кода бесплатны. Никто не заплатит за то, что кода получается меньше.
2) Написание кода, в частности нового — это не такая уж большая часть работы разработчика.
3) Куча техник современных техник борьбы со сложностью — DDD, CQRS, Event Sourcing требуют в разы больше дополнительных строчек. Да, это борьба со сложностью, и да, сложность и количество кода могут обратно коррелировать.
4) Ну там и не преувеличивайте количество работы с разделёнными слоями, это не так уж много кода даже в момент его написания.
UFO just landed and posted this here

Можно далеко идти в архитектуру, но зачем? У меня простые утверждения

>>из репозитория нельзя выставлять IQueryable, а это мега удобно. 

>Не знаю специфики вашей платформы. Если я правильно понимаю, это что-то вроде Query Builder. Да, это должно быть отделено от бизнес-логики.

Да это Query Object и это все еще удобно. На ваш аргумент "это должно ..." я ответу еще более мощным "это не должно ..." Чем внезапно repo.FindById оказался лучше db.FindById ?

>>Каждый клиент теперь зависит от мока. Значит как только в репо изменится сигнатура какого-то метода получим некоторый кайф.

>У вас же статический ЯП? То есть даже тесты смотреть не нужно.

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

И цепочка рассуждений такая: TDD требует тестировать практически каждый класс. Без моков этого сделать нельзя. Следовательно везде будут интерфейсы и DI. а также обертки над любом API которое делает IO. А это почти все, не побоюсь утверждать, что это 90% всего что есть в природе. Как следствие на моки будут завязаны почти все тесты. Изменение любого внутреннего интерфейса приводит к каскадному эффекту - ломаются моки в тестах клиентов этого интерфейса.

Какое из утверждений выше не истинно?

тестировать практически каждый класс. Без моков этого сделать нельзя

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

Если класс А зависит от репо, то мне нужен мок. И так по цепочке. Классу Б нужен класс А, либо мокать А, либо мокать репо. Как с этим бороться?

Запускать тесты в изолированном контексте, где репо заменён на мок, и всё.

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

>>>Вот тут не правда. Тестировать-то можно и без тотального мокирования.

>>Если класс А зависит от репо, то мне нужен мок. И так по цепочке. Классу Б нужен класс А, либо мокать А, либо мокать репо. Как с этим бороться?

>Запускать тесты в изолированном контексте, где репо заменён на мок, и всё.

"вы либо трусы наденьте либо крестик снимите" :)

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

  • Тестировать-то можно и без тотального мокирования.

  • Нужно мокать все, что делает IO

  • Примерно весь код типичного веб приложения так или иначе завязан на IO (через репо или иные обертки)

Могут быть все одновременно истинными.

Мне кажется вам стоит представить более весомый пример, чтобы показать как можно "Тестировать-то можно и без тотального мокирования." Игрушечный пример, в котором 1 класс тестируется и нет IO ничего не говорит о реальной жизни. В реальной жизни у нас куча классов делает IO, и большинство остальных на них завязаны.

Вообще-то тут не 1 класс тестируется, а поднимается реальное приложение состоящее из сотни модулей, которое косвенно работает с локальным и сессионным хранилищами, консолью, строкой адреса, клавиатурой, мышью, ДОМ-ом и браузерным рендерингом.

В качестве примера силы TDD вы привели приложение, которое было создано в 2016 году, а первый тест у него появился в 2018. Вот это test first!

О!!!! так вы же и автор, и после этого вы мне за TDD втираете?

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

Что я думаю про TDD вы можете понять из этого видео:

А в этой статье подробно разбирается упомянутая вами номенклатура.

Какое из утверждений выше не истинно?
До истины далеко, пока работаем над эвристиками.
repo.FindById оказался лучше db.FindById
Локализованностью конкретного функционала. Если вам нужно изменить выборку по ID для какой-то сущности (добавить ещё один фильтр или выбирать из key-value), то это сводится к одному месту. Ещё более важно, если вы хотите узнать, как получается сущность из БД. И вообще, видя репозиторий можно узнать, как это приложение взаимодействует с БД для этой сущности. В одном месте все выборки для сущностей, легче найти, легче вспомнить. Это обеспечение high cohesion.
Пользы от этого действия 0, это дань архитектуре.
1) Пользы in se ipso от починки моков нет, но есть польза в наличии тестов, которые используют моки.
2) Починка моков довольно дешева. Если у вас репозиторий для каждой сущности, то он используется в ограниченном количестве мест (например, в отдельно взятой слоистой архитектуре — только на стыках слоёв Application и Domain, где и создаётся доменное окружение).
3) Изменение сигнатур намного чаще касается тестирования обычных юнитов, как сущностей, которые по своей природе (в самом распространном примере архитектуры) не имеют внешних зависимостей. То есть ломает код чаще всего изменение изолированных объектов.
4) Кроме того, даже если передавать не мок, а сам объект, подключенный ко внешнему миру, то изменение его сигнатуры точно так же ломают тест. Мок только реализует ту же самую сигнатуру.

Т.о. 3 и 4 делают это аргументом против тестов, дескать, изменения в коде ломают тесты даже без ошибок, а значит тесты кроме, возможно, приёмочных (acceptance) не нужны. Но цель тестов, даже приёмочных — именно в том, чтобы ломаться при изменениях (если они что-то проверяют), аргумент против тестирования вполне отвечен и я не вижу смысла на него тратить ещё больше время в этом топике.
TDD требует тестировать практически каждый класс. Без моков этого сделать нельзя
В целом, можно обойтись вообще без моков и давать подключенные ко внешнему (тестовому) окружению объекты. Никак не противоречит TDD. Но это:
1) Дороже покрытие многих классов функциональности. Pure code намного быстрее. Лучше на дорогие тесты отвести менее полные проверки, то есть проверять ими уже проверенные кирпичики.
2) Менее удобно создавать фикстуры для БД, фикстуры файлов, состояния очередей и т.п. Когда я разрабатываю что-то без всякого TDD, мне часто намного проще начать с теста, чтобы сделать рабочий компонент кода, чем воссоздавать для проверки соответствующего ему функционала всю среду.

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

>Какое из утверждений выше не истинно?

До истины далеко, пока работаем над эвристиками.

Удобно, я вам даю набор предикатов и чтобы все расставить по местам достаточно показать, что один из них неверен, не стоит ради этого такие сочинения писать.

Локализованностью конкретного функционала.

Вы сравниваете public User FindById() с public static User FindById(this DbContext db). Какая тут разница в локализации функционала?

4) Кроме того, даже если передавать не мок, а сам объект, подключенный ко внешнему миру, то изменение его сигнатуры точно так же ломают тест. Мок только реализует ту же самую сигнатуру.

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

а значит тесты кроме, возможно, приёмочных (acceptance) не нужны.

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

Но цель тестов, даже приёмочных — именно в том, чтобы ломаться при изменениях (если они что-то проверяют)

Ломаться при изменениях видимого поведения системы. Это совсем не то же самое, что ломаться при любом серьезном изменению любого интерфейса.

Вопрос в эффективности и в борьбе со сложностью.

Вот смотрите, я беру код вашего repo.FindUserById и перемещаю его в статическую функцию. Затем я выкидываю сам репо за ненадобностью и все тесты с ним связанные. Так как и у вас и у меня есть тесты самого API, покрытие после этого осталось таким же. Я выкинул кучу кода и... усложнил систему и навел беспорядок? Какое такое определение сложности и порядка нужно принять, чтобы это оказалось истинным?

UFO just landed and posted this here

Аргумент вида "в пост мясо есть нельзя, в писании так сказано". Как только появится определение abstraction в виде понятного формализма, мы сможем серьезно об этом говорить. А пока я могу взять два куска кода, предположить возможные изменения требований и сравнить как они будут меняться. Несложно увидеть, что конкретно в этом случае репо сосет и будет меняться на каждый чих (вместо с тестами на него)

GET /users?email=me@me.com
dbContext.Users.Where(x => x.Email == email).ToJson();
userRepo.FindByEmail(email).ToJson();

изменения

  1. Новые фильтры

  2. Сортировка по разным полям

  3. Ограничение на то, каких юзеров можно и нельзя видеть

  4. Включить/выключить список ролей юзера в ответе

GET /users?email=me@me.com&name=Peter&role=1
var users = dbContext.Users.ScopedBy(user.Role)
.Where(x => x.Email == email && x.Name == name);
if (role) users = users.Include(x => x.Roles);
return users.ToJson();

Я тут сортировку не добавил, думаю вы сами догадаетесь как ее можно сделать.

Попытка все это реализовать с помощью репо приведет к созданию кривой версии IQueryable, которую еще придется поддерживать. Это утверждение, можете меня поправить, если не так.

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

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

На основе абстрактного апи никакого кода и не будет. Будет простой фасад преобразующий любой http-запрос в запрос к этому самому апи.

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

На 40 сущностей "копипасты" будет около 1000-2000 строк. Ваш фасад + конфиг будет сильно больше и его будет сильно сложнее менять, не находите?

Другой большой вопрос в том, что будет с вашим фасадом, когда появятся требования что-то кешировать, где-то рассылать нотификации и всякое такое. В случае с "копипастой" я допишу по строке там где нужно. И даже могу что-то зарефакторить и спрятать в сервис. А в случае фасада вы будете допиливать свой DSL. Ради экономии примерно 1 строки на каждую сущность :)

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

Да, это всё в конфигурацию выносится. Это всего несколько сотен строк, которые и тестировать не надо.

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

Допиливание нормального DSL делается в пару строк кода - это не рокетсаенс. Впрочем, у нас было и кеширование, и нотификации, и даже выдача с правами по всем полям, чтобы клиент знал что можно давать пользователю редактировать, а что - нет. Всё это через 2 фасада: REST и WS. Во втором случае ещё и обновление данных в реальном времени приходили.

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

Одно из таких абстрактных API это OData, а вот это

cloc AspNetCoreOData-master 

Language               code

C# 107805

намекает, что тезис про простоту тула не очень согласуется с реальностью.

Да, это всё в конфигурацию выносится. Это всего несколько сотен строк, которые и тестировать не надо.

Узкоспециализированный тул (5K - 10K LoC) + несколько сотен строк конфига против 1K - 2K LoC единообразного кода, который осилит любой джун. Выигрыш не всегда очевиден.

Я уже даже не помню, зачем я в это обсуждения ввязался, наверное категоричность заявления про абстрактное апи меня задело. И "копипаста" и абстрактное апи имеют место быть. Для меня, когда-то было открытием, что пачка тупых контроллеров в поддержке часто оказывается дешевле, чем если тоже самое делать с наворотами ради DRY.

Одата сильно переусложнена. Не берите с неё пример.

Где вы там 5к строк в туле нашли? Там 1к в прыжке.

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

UFO just landed and posted this here

Репо ограничивает набор запросов которые требуется качественно реализовать.

Если вы выставляете IQueriable то вы требуете от реализации успешно выполнять не только запросы, которые используются приложением но и все остальные мыслимые запросы. Соответственно подмена хранилища будет сложнее.

Оба решения обладают как достоинствами так и недостатками.

UFO just landed and posted this here

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

Если у вас появились моки, то у вас уже не юнит тесты.

Когда у вас when(что-то).thenReturn(что-то) то вы описываете состояние. А потом ещё и всякими хитрыми путями выдираете промежуточные и финальные состояния чтобы проверить правильность кода. И тест потихоньку становится едва ли не сложнее кода, который он тестирует.

Где-то в этот момент фанатики TDD тихо отходят в сторонку или начинают вещать о неправильном коде. Не, ну калькулятор можно и строго по тдд написать…

UFO just landed and posted this here
Разве TDD запрещает обеспечивать часть 100%-ого покрытия интеграционными тестами? Обсуждение на кучу комментариев соломенного чучела.

Слышала и пробовала, тут все же профессионалы собрались. Я пишу биржу, 80% моего кода активно работает с БД, а 20% многопоточка. Подскажите, как мне замокать базу и потоки? Тестировать что класс А вызвал класс Б не интересно. Интересно увидеть, что в результате получился корректный SQL, а база в ответ на него вернул то, что ожидается.

UFO just landed and posted this here

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

Чем более низкоуровневая изоляция, тем больше требуется ресурсов для исполнения теста. Вариантов борьбы с этим 3 в зависимости от предметной области:
- Замена реальной зависимости некоторой легковесной заменой (моком, адаптером или даже фасадом). Отдельное тестирование мока на соответствие реальной зависимости. И отдельное тестирование остального кода с этим моком.
- Разработка легковесных способов изоляции (контейнеры вместо виртуализации, как пример).
- Построение архитектуры таким образом, чтобы более глубокая изоляция не требовалась. Например, если некоторый код не лезет в глобальное состояние, а работает лишь с локальным, то достаточно только автоочистки всей выделенной в рамках исполнения памяти, и не нужно перезагружать ОС после каждого теста.

Согласен. Но почему-то все говорят о xDD, вместо обсуждения реальных проблем. О том, как можно изолировать базу, как сеть, как HTTP и так далее. Обычный ответ - моки, но это не ответ вовсе, а уход от проблемы.

Ну в общем да. Скажем, как только вы пытаетесь заменить что-то типа СУБД моком, все и ломается. Или даже есть такой API, JDBC, взаимодействие Java-база. Так вот, как правило попытка замокать его — это очень больно, очень. И совершенно неэффективно. Поэтому как правило этот низкоуровневый протокол оборачивают во что-то типа DAO, и уже тестируют их.

Зря только клавиатуру мучили. Не нравятся тесты не пишите. Требуют на работе, а вы не хотите - смените проект. Только не надо целое направление IT технологий подвергать сомнению.

Кроме TDD нет подходов по-вашему?

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

Чтобы что? Чтобы он не был проще, чем юнит-тесты? А это точно правильный подход, усложнять код ради тестов?

UFO just landed and posted this here

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


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


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

UFO just landed and posted this here

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

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

Так я о том же. 1000->3000 это ещё совсем неплохо, иногда такое разбитие может в 10 раз увеличить кол-во кода.

Тестируемость кода обычно сильно коррелирует с модульностью и изоляцией, с иерархичностью компонентов. Плохо тестируемые участки либо таковы из-за слишком высокой связанности, либо из-за сторонних эффектов (сторонние эффекты во «внешний мир» стоит выделять из основной логики, тестировать только связующий слой в тонких интеграционных тестах). Адаптация к тестируемости — это обычно не игра с нулевой суммой, код с высокой связностью (coupling) в принципе сложнее сопровождать, плюс такой код менее стабилен для изменений. В принципе, 80% жизненного цикла кода — это и так вовсе не его написание, а сопровождение.
Состояние тестов и форма самой пирамиды тестирования, впрочем, зависит от характеристик приложения. UI и базовый CRUD будут иметь другие формы тестирования.

А если присмотреться к хорошо юнит-тестируемому коду, то зачастую возникает ощущение, что это не код, а обвязка для юнит-тестов, которая совершенно факультативно ещё и что-то полезное умеет делать. 8)) Вплоть до того, что ВЕСЬ код состоит исключительно из шаблонов для покрытия всего и вся юнит-тестами, не сильно страдая по скорости (правда, сколько оно собирается — думать страшно).

А если присмотреться к хорошо юнит-тестируемому коду, то зачастую возникает ощущение, что это не код, а обвязка для юнит-тестов
Ну что я могу ответить на ваши ощущения. У меня их нет, и я знаю, для чего делается то, или иное. Тестируемость в этом, впрочем, никогда не имеет первостепенного значения, а возникает как побочный эффект. Самая удобная парадигма для тестирования, к примеру, получилась потому, что первостепенная задача задача была — предсказуемость поведения. Я говорю, конечно же, о Haskell.

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

А я, конечно же, не о нём (никогда ничего промышленного на хаскелле не писал). Зато можно покрыть всё контрактами и доказать через gnatprove. Я, конечно же, об Ada. 8)) Вот только это частные случаи.


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

Следующее же предложение. Халвы всему, живьём я это не видел, на интервью рекламировали подход...

Зато можно покрыть всё контрактами и доказать через gnatprove
Есть примеры и посвежее, как Idris или ещё новее Kind, но я не знаю, как автопруверы могут быть применены в обычной разработке в обычных языках. Никакие особо доказательства и не применяются. Но вот изолирование эффектов, как в Haskell применяется в разработке очень часто и к этому сводится изрядная часть борьбы со сложностью, и вовсе не ради тестируемости. В хаскеле это тоже не ради тестируемости. Собственно, некоторые адепты дизайна кода в статьях нередко обращаются к строгим ФП и хаскелю. Так что критиковать нужно не TDD, а распространённые подходы к борьбе со сложностью. Реализованный по типичному пути DDD отлично тестируется, но имеет слабое отношение к TDD.
Так что аналогия неравнозначная.

Частично правы - как и автор статьи, но надо меньше эмоций и больше акцентов в доводах.

Извините если что

У меня аналогичные ощущения от всей этой затеи с TDD. 100% покрытие кода,не гарантирует 100% правильности работы, а в сотнях и тысячах тестов будет масса ошибок, которые нужно исправлять отдельно.

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

в сотнях и тысячах тестов будет масса ошибок

Хммм... Кажется, у меня в уме прямо сейчас зарождается парадигма TDTD - Test Driven Test Development. Иначе как кошерно проконтролировать корректность тестов, если не тестированием тестов? :)

Я очень надеюсь, что постепенно разовьются подходы автоматической валидации\верификации кода, вроде линтеров, статических анализаторов, TLA+/TLC и т.п. И после этого фундаментально сократиться кол-во тестов, которые нужно будет писать. А с развитием ЯП сложность написания любого кода, в том числе тестов уменьшиться.

Мутационные тесты ещё есть

Тесты тестируются кодом, который тестируют тесты. Это очень быстро подмечаешь, когда пишешь тесты правильно (а не только для того, что бы они только проходили).

100% покрытие кода, не гарантирует 100% правильности работы
Любопытно, что тезис про гарантии полной корректности выдвигают только противники TDD, для его последующего опровержения. =)

TDD разве не подразумевает, что мы пишем новую функциональность только тогда, когда имеем красный тест на неё? Это автоматически означает 100% покрытие.

Но я то говорю про то, что люди зачем-то оспаривают тезис о полной корректности («100% правильности работы»).

На одном проекте лет 5 назад пришлось столкнуться с ситуацией, похожей на описанной автором статье: в проекте было близкое к 100% покрытие unit-тестами и ноль интеграционных и функциональных, причем TDD-адепты были категорически против их написания

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

Когда впоследствии TDD-адепты не могли обеспечить 100% ковераж чистыми юнит-тестами, в ход шли моки, фейки и стабы в настолько невероятном количестве, что юнит-тест фактически тестировал только мок-машину, а само приложение только прирастало багами, не говоря уже об уродовании кода ради удобства моков

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

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

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

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

Если код пишется вперед тестов - это не TDD

Я это и написал. Сначала тесты, потом код. Только это при мне никто никогда не соблюдал.

Сначала тест. В единственном числе. Потом код. Потом следующий.

Что сразу прибивает нас гвоздями к разработке снизу вверх. Вот лично я не уверен, что это такой уж однозначно хороший вариант, мягко говоря...

Извините, но этот top-down выглядит как костыль. Груда моков, которые потом выкидывать...

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

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

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

Наверное имелось ввиду "НЕ используя TDD"?

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

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

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

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

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

Да, из хороших кирпичиков можно собрать плохой дом. Но хороший дом проще создать из хороших кирпичиков.

Да, сами авторы идеи говорят, что нет смысла в 100% кода и оно только вредит.

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

Я люблю тесты, пишу их сам и заставляю других по возможности, у меня проблема только с TDD

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

Возможно не понял.

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

Да, из хороших кирпичиков можно собрать плохой дом. Но хороший дом проще создать из хороших кирпичиков.

Ещё раз, я ЗА юнит тесты, они почти всегда уместны и помогают

Да, сами авторы идеи говорят, что нет смысла в 100% кода и оно только вредит.

Насколько я помню авторы идеи говорят, что код пишется только после того, как написан тест, это разве не подразумевает, что весь написанный код покрыт тестами и что автоматически дает 100% покрытия?

Я люблю тесты, пишу их сам и заставляю других по возможности, у меня проблема только с TDD

Я тоже в общем-то большую часть жизни так работал. Пишешь код, потом несколько дней на тесты. И это обычно очень унылые несколько дней; я очень ответственный человек, поэтому пишу несмотря на то, что мне при этом скучно и уныло.

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

Так вот самым сильным аргументом для того, чтобы попробовать TDD еще раз, на этот раз с наставником, стало то, что TDD is more fun. Писать тесты вместе с кодом - занятие гораздо более веселое и интересное. И для меня это реально оказалось так.

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

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

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

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

UFO just landed and posted this here

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

UFO just landed and posted this here

Во-первых, в вашем примере вы начинаете реализацию класса не понимая какие у него зависимости и на каком уровне/слое он находится. Как раз тут TDD вам сигнализирует о том, что вы, возможно не достаточно продумали решение и не понимаете до конца что хотите получить.

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

Андроидам на заметку:

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

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

что, TDD противоречит

Ссылка, цитата?

UFO just landed and posted this here

"Рефакторинг - процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения"

UFO just landed and posted this here

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

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

UFO just landed and posted this here

Т.е. вы делали рефакторинг, запустили тест, поняли, что ваш рефакторинг сломал компонент, потому что тест не прошел, и решили изменить тест. Простите, при чем тут TDD?

UFO just landed and posted this here

Если Вы не меняете поведение, то Вы и не меняете тест. И он сразу покажет, что Вы накосячили :)

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

UFO just landed and posted this here

Рефакторинг это изменение терминологии в котором написано поведение.

Есть рефакторинги с раной степенью риска (например c использованием автоматических рефакторингов), можно делать рефакторинги в два этапа (код, потом тесты). В целом проблема есть, да. Можно писать только E2E тесты и для каких-то случаев это работает и тесты не будут изменяться при рефакторинге. Но будут изменять при изменении UI.

Вопрос, что лучше. С учетом всех плюсов и минусов

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

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

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

Я не ярый адепт TDD и не холивара ради. Исключительно из личных наблюдений за 11 лет опыта в разработке:

  • Код, покрытый юинт тестами обычно имеет лучшую архитектуру, чем тот, который тестов не имеет

  • Делать изменения в покрытом тестами компоненте безопаснее

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

  • Громоздкие тесты, которые сложно читаются и понимаются сигнализируют о проблеме в юнитах, которые они тестируют

Да, понятия хорошая/плохая архитектура очень широки, но для краткости позволю себе оставить их без пояснения.

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

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

Соглашусь. Но тут, как и везде, нужен баланс. Иногда практичнее писать до, иногда после.

Как обычно, проблема не в методологии, а в фанатиках.

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

UFO just landed and posted this here

TDD позволяет написать то, что без TDD написать практически невозможно)

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

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

На счёт 100% покрытия - это бред какой-то или фантастика.

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

У меня на практике обычно вот эта часть, что вы назвали "через добавление 10 ифчиков, а потом заменяете это на нормальный алгоритм" вызывала трудности, т.к. "нормальная" реализация всегда приводила к переименованию\удалению интерфейсов\классов\методов\аргументов, перестройке всей структуктуры кода. После чего все ранее написанные тесты грубо говоря проще выкинуть и написать заново. Как вы с этим боролись?

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

UFO just landed and posted this here

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

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

UFO just landed and posted this here

Один хороший тест - как правило совсем не юнит.

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

У меня была задача (фронтенд) – сделать инпут БИК банка с асинхронной валидацией и автозаполнением данных названия банка/кор. счёта через ответ от сервера. Я дня 3 откладывал задачу, потому что не мог найти нужную точку, с которой было бы удобно писать тест. Изначально думал, что начать надо с валидатора – практически чистая функция) Но валидация в Angular настолько специфична, что такие тесты ничего не опишут. В итоге нашёл удобный масштаб – стал тестировать класс формы BankFormGroup целиком. Удобно описывать кейсы, которые эмулируют действия пользователя.

// test: should fill bank data
const fg = new  BankFormGroup(checkBicApiMock);
fg.controls.bic.update('123456789');
expect(fg).toBeInvalid();
expect(fg).toBePending();
wait(1000) // ждём, пока проверка БИК выполнится
expect(fg).toBeValid();
expect(fg.bankName).toBe("Название банка от сервера");


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

У меня на практике обычно вот эта часть, что вы назвали "через добавление 10 ифчиков, а потом заменяете это на нормальный алгоритм" вызывала трудности, т.к. "нормальная" реализация всегда приводила к переименованию\удалению интерфейсов\классов\методов\аргументов, перестройке всей структуктуры кода. После чего все ранее написанные тесты грубо говоря проще выкинуть и написать заново. Как вы с этим боролись?

Декаплингом тестов от имлементации (тест не должен знать, как именно тестируемая фукциональность имлементирована, т.е. не должно быть verify (проверок вызова метода зависимого класса внутри тестируемой функции) и очень помогает сильно ограничить использование автомокеров). При этом подход к тестам довольно сильно меняется. Вместо того, чтобы писать тестовый класс на конкретный класс реализации (например, Multiplier -> MultiplierTest, 1:1), тест пишется на конкретную функциональность ("умножение чисел").

Функциональность - это как раз то, что скорее всего будет оставаться гораздо более стабильным, чем реализация; ведь вы эту фичу не просто так добавили, а потому что бизнес/пользователи попросили. Вы можете и скорее всего дальше будете её усложнять, но нечасто просто выкините саму фичу из того, что делает ваша программа.

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

Хороший пример того, как это делается, можно посмотреть в TDD курсе у J.B. Rainsberger (JBrains).

Конечно тут же встает вопрос, что такое юнит и что такое юнит тест, и в этот холивар я точно лезть не хочу : )

А не хотите статью запилить с объяснением, примерами и особенностями подхода, по комментарию не совсем понятно как это?

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

Тут нужно показывать не самый простой проект, написанный по TDD с тестами, которые ничего не знают про имплементацию проверяемой функциональности, то, как написаны тесты и то, как они при этом помогают рефакторингу. Из того, что на эту тему есть в сети, мне JBrains с лету приходит в голову вышеупомянутый (у него есть курс The World's Best Intro to TDD, действительно очень качественное введение в TDD), хотя наверняка есть и другие примеры. Но они вряд ли будут маленькими.

Я чтобы этому научиться уже не первый месяц в свободное время пишу код в паре с людьми, которые давно (некоторые по 20 лет и больше) в этой тусовке и участвую вместе с ними в mob programming сессиях.

Поэтому я тут наверно только могу порекомендовать, куда копать, если дальше интересно. Копать в сторону decoupling тестов от имплеметации. Про это есть целая школа TDD (Chicago, classicist). Можно про них почитать, можно посмотреть курс JBrains, у которого хорошо показаны тесты на фукнциональность которые не завязаны на конкретную имплементацию, есть хорошая статья про это у Martin Fowler "Mocks Aren't Stubs" (https://martinfowler.com/articles/mocksArentStubs.html), и еще вот тут есть отличный ответ на ту же тему от perfectionist https://softwareengineering.stackexchange.com/questions/5898/how-do-you-keep-your-unit-tests-working-when-refactoring (начинается "Contrary to the other answers...").

Еще из людей, у которых скорее всего это можно увидеть на видео, вроде бы у Uncle Bob были примеры, он тоже классицист TDD-шный, хотя я не особый его фанат. У Geepaw Hill есть хорошие видео. Еще приходит на ум James Shore, у которого тоже много хороших видео в свободном доступе и можно найти примеры хорошего кода на JS с TDD.

видео
посмотреть
видео
видео
видео
видео

А-а-а-а-а-а!!! звук роскомнадзора об стену

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

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

UFO just landed and posted this here

Да, я схитрил, и одним предложением заменил целую эпопею) На меня сильно повлияла вот эта статья про отказ от зависимостей https://habr.com/ru/company/jugru/blog/545482/

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

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

Пример из практики – многостраничная форма. (Да, я тупой/модный фронтендер)

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

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

Поэтому сначала пишем через TDD маленький и простой класс, отвечающий за навигацию по шагам: нужно знать какой шаг активный (`+getActiveStep()`), на какой шаг можно/нельзя перейти (`+isStepAvailable(stepId)`). Используем минимальный интерфейс, который нужен только для этой функциональности – получаем игрушечный класс Stepper и крохотный интерфейс Step. Начинаем использовать нашу игрушку в большом грязном классе MainStepper. Да, большой грязный класс остаётся без тестов, но ответственность за функциональность навигации остаётся в нашем игрушечном классе и хорошо протестирована.

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

Почему большой класс оставется без тестов? Можно хотя бы E2E/Integration тест написать, который его покроет?

Ни в коем случае! TDD строго запрещает другие виды тестирования!)))

UFO just landed and posted this here

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

UFO just landed and posted this here
  1. 100% покрытие кода, может быть это "пушка" для педанта, но в живых проектов 60-80% этого более чем достаточно (сужу по свои кейсам).

  2. TDD как подход к реализации... Лично мне не зашёл, поскольку чаще всего прибегает "бизнес" и дедлайны уже вчера ))) в итоге рефакторинг

  3. А вот с посланниками TDD под калькулятор - тут 100 балов))))

UFO just landed and posted this here

Видимо я не верно выразился либо вы не так прочитали, "67%" я имею ввиду не успешность тестов, с 60-70% покрытия тестами приложение. Иными словами ~30% кода приложения не нуждаются в тестах.

UFO just landed and posted this here

В таком случаи, коллега, мы говорим об одном и том же с разных точек обзора :)

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

  • Лучше всего писать по TDD код, реализующий бизнес-логику. Инфраструктурный код, контроллеры, UI и прочее поддается хуже; в этих случаях я не пытаюсь в TDD

  • Как следствие предыдущего пункта, для работы по TDD в приложении уже должна быть кое-какая инфраструктура, готовая к тестированию; писать по TDD с нуля не особо заходит

  • Перед написанием тестов нужно определить их тип. Я разделяю юнит- и интеграционные тесты. Чтобы не увязнуть в моках, следую правилу - если в модуле нет зависимостей от внешних ресурсов, то пишется юнит тест (соответственно, юнит тесты никогда не содержат моков); иначе пишется интеграционный тест, в котором моками заменяются только внешние зависимости, которые невозможно/неудобно поднимать локально (например, СУБД всегда использую реальную)

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

UFO just landed and posted this here

А сам файловый логгер тестировать надо?

А то, что в приложении в целом используется файловы логгер?

UFO just landed and posted this here

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

UFO just landed and posted this here
UFO just landed and posted this here

Ну вот, сейчас ещё и руки оторвут..

const context = $.$ambient({
    
    $log: function( ... what ) {
        
        this.$elastic.send({
            type: 'log',
            when: Date.now(),
            what,
        })
        
    },
    
})

context.$app_start()
UFO just landed and posted this here

Судя по вопросу статью вы не читали. Вы и с коллегами так общаетесь?

UFO just landed and posted this here

И тоже считаете не своей вотчиной ходить по ссылкам и узнавать что-то новое самостоятельно?

UFO just landed and posted this here

Даже если в них рассказывается про паттерн, который может быть применён в любом языке? Ну ок.

UFO just landed and posted this here

Вы не просекли фишку, попробуйте ещё раз. С глобальным контекстом обычно никто не работает.

UFO just landed and posted this here

А потом эти же люди удивляются, почему это вдруг несчастная страничка текста на сайте отжирает 4гб памяти и целиком одно ядро. 8))

A это здесь причём? Какое это имеет отношение к тому что вещи вроде конкретной реализации вашего логгера действительно логичнее всего прятать за фасадом/интерфейсом?
UFO just landed and posted this here

Ничего не даётся бесплатно. Поэтому квантор всеобщности там явно лишний. С одной стороны чаще всего накладными расходами можно пренебречь, с другой — иногда так посмотришь, как тут пренебрегли, и вот тут, и ещё в 100500 местах, а в итоге разбор какой-нибудь не особо развесистой pdf'ки poppler'ом занимает секунды, а то и десятки секунд.

Ничего не даётся бесплатно. Поэтому квантор всеобщности там явно лишний.

Мне интересно что вы понимаете под «накладными расходами» в случае с интерфейсом.

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

Но я не могу себе представить таких сценариев в контексте вашего заявления про «страничка текста на сайте отжирает 4гб памяти и целиком одно ядро.»

тут пренебрегли, и вот тут, и ещё в 100500 местах, а в итоге разбор какой-нибудь не особо развесистой pdf'ки poppler'ом занимает секунды, а то и десятки секунд.

И сколько из этих «десятков секунд» по вашему будет «потеряно» из-за того что кто-то где-то использует интерфейсы?
Мне интересно что вы понимаете под «накладными расходами» в случае с интерфейсом.

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


Но я не могу себе представить таких сценариев в контексте вашего заявления про «страничка текста на сайте отжирает 4гб памяти и целиком одно ядро.»

Это всё-таки по бОльшей части (на 99.9%) шутка.


И сколько из этих «десятков секунд» по вашему будет «потеряно» из-за того что кто-то где-то использует интерфейсы?

Чистых интерфейсов там, емнип, нет, но наследование во все поля и прочие лучшие (или которые когда-то считались лучшими 8)) ) практики. Давно дело было, да и мерять такое тяжело. По ощущениям, оверхед там заметную долю составляет просто в силу устройства pdf, вызовы всяких виртуальных методов там на каждый чих.

Отлично, вместо простого файлового логгера мы получили или один универсальный (который и в файл писать умеет, и в БД, и в systemd, и по сети отправлять в какой-нибудь сервис), либо получили простой интерфейс-обёртку + пачку реализаций, одна из которых — тот самый файловый логгер. Что дальше-то?
либо получили простой интерфейс-обёртку + пачку реализаций, одна из которых — тот самый файловый логгер. Что дальше-то?

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

А сами конкретные реакизации логгера вы можете тестить отдельно. Или не вы, а те кто их написал.
UFO just landed and posted this here

Вообще, консольный вывод программы можно легко направлять хоть в файл, хоть в сеть, хоть в dev/null. И это даже куда более гибкое решение, чем фасад и куча плагинов.

UFO just landed and posted this here
Вообще, вовсе даже не очень легко. Логирование — это не только строчка с текстовыми описанием, а порой и обилие дополнительного контекста. Парсить что-то из консольного вывода — часто так себе идея.
Теоретическая и порой практическая возможность есть, и, более того, 12 factor app предписывает так делать. Но на практике получается, что вместо превращения обильных структурированных данных в текстовый формат и обратно, проще послать сразу структурированные данные куда следует. В Sentry, например. Читать глазами то, что нужно Sentry, всё равно бесполезно.

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

Я вам сейчас глаза открою: не все проекты (особенно довольно низкоуровневые и/или высоконагруженные) используют готовые фреймворки.
Более того, существуют проекты, которые действительно логируют только в один файл (максимум в syslog ещё). Если вам надо — натравливайте на этот логфайл что вам хочется: хоть дампилку в БД, хоть отправлялку по сети, хоть grep. Если интересно — посмотрите на всякие Asterisk и иже с ними.

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

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

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

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

Как мне кажется, сложность с TDD нет, не так.

Как мне кажется, сложность с восприятием TDD во многом заключается в том, что вокруг этой концепции навалено множество смежных идей. "Идеальное 100%-ное покрытие", "тесты - это замена документации", и так далее. Спасибо дядюшке Бобу, как говорится. Они выросли из самой идеи автоматического тестирования, но непосредственно к TDD имеют достаточно слабое отношение. Попробуйте устремиться к 100%-ному покрытию, создавая при этом тесты после кода, и большинство проблем никуда не денется.

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

Вот только сходу, без опыта, бросаться на тестирование многопоточного кода - это верный рецепт получить тяжёлую психологическую травму (сочувствую). Лучше всего начинать с code kata или пробовать силы на маленьком стороннем проекте. Каты дополнительно хороши тем, что их можно гонять в качестве быстрой утренней разминки. Выделил 20 минут, провернул за это время десяток-другой red-green-refactor циклов, стёр код и пошёл работать. На сложном рабочем коде такое количество циклов может занять гораздо большее время, а опыта в TDD принесёт не сильно больше.

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

+1 к "Вот только все эти абсолюты к самому TDD имеют мало отношения, разве нет".

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

Но у вас реально очень превратные представления о TDD, которые к реально практике отношения имеют очень мало от слова совсем.

  1. 100% покрытие вообще не является целью TDD. Это глупо и никому не нужно, потому что вот именно в этом случае придется писать совершенно нелепые тесты на сеттеры и геттеры и прочую ерунду. См. ссылку на то, как пишет тесты Кент Бек, которую давали выше. Никто из тех, кто работает по TDD из тех, с кем я лично общаюсь, не считает 100% покытие полезным, более того, в целом тестовое покрытие - метрика неоднозначная и проверяет только то, что бранч был вызван. Ничего про качество теста, вплоть тупо до наличия ассерта, оно не говорит.

  2. Тесты как документация не является целью TDD! Совсем. Вы не с BDD перепутали?

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

  4. "Да, и что бы 2 раза не вставать замечу, что когда тесты покрывают код целиком и полностью, не оставляя живого места, любая попытка изменения функции или ее интерфейса приводит к дикому баттхёрту." - а вот это очень интересная тема. Дело вообще не в тестах, дело в моках. Есть две школы TDD, Чикагская и Лондонская. Чикаго - "мокисты", которые изолируют каждый класс и мокируют все зависимости. Лондонская школа избегает того, чтобы тесты знали, КАК метод делает то, что он делает - они проверяют только результат! И при таком подходе тесты - первейший инструмент рефакторинга, который помогает менять код, не ломая его. В этом собственно и основной смысл тестов.

  5. "TDD позволяет сформировать хорошую архитектуру на ранних этапах, которую потом почти не придется менять (как и код), ибо TDD заставляет подумать до написания кода". TDD помогает писать код, который легко тестировать (юнит тестами, да). Задачи не менять её не стоит вообще! Совсем наоборот, при добавлении любой фичи - смотришь на то, а не поменялось ли чего более глобально, как эту фичу встроить; рефакторишь всегда! И тесты, если ты их сохранил гибкими и не знающими имлеметации того, что они тестируют, очень в этом помогают.

  6. "Юнит тесты хорошо, а другие – плохо". Ну блин. Откуда вы это взяли? То, что есть тестовая пирамида, не означает, что не нужно ничего, кроме юнитов. И опять же, TDD тут мало при чем.

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

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

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

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

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

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

Да, там неплохие комментарии ;)

Упс, только я тоже ежа с апельсином спутал выше. Про mockist vs classicist TDD школу, мокисты - это Лондонская школа. : ) Чикаго - classicist и как раз они тестируют все через state и избегают знания имлементации. Сорри!

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

James Shore очень толковый чел, +1.

Существуют возможности делать наброски даже по TDD, нащупывать первоначальную архетектуру — которую потом тридцать раз может быть поменяешь

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

отлично написано, тот случай когда каменты великолепны )

А не находите ли аналогии ТДД с процессом научного поиска, где перед тем как начать чтот делать, надо прикинуть критерии проверяемости и какими экспериментами проверять, что получилось)

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

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

UFO just landed and posted this here

Ещё больше печально, что адепты юнит-тестирования собственно тестирование не понимают вообще.

UFO just landed and posted this here

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

UFO just landed and posted this here
UFO just landed and posted this here

Ну или включите им это медитационное видео:

Просто из доброты душевной, что б добить: а если Вы попробуете этой половине очень подробно и понятно объяснить юнит-тестирование, то выясните, что из второй половины, которые понимают, минимум 3/4 понимают неправильно (ну, по крайней мере, не так, как Вы) 8))

UFO just landed and posted this here
"Неправильное понимание" я тоже отношу к "просто непониманию". :)

Я к тому, что оценка "понимающих" может быть сильно завышена. 8))


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

Спасибо, полезно. Хотя местами несогласен:


1.


Для меня предел это всего 2-3 приватных метода в классе, хотя я вообще любой приватный метод, даже если он всего один, всегда рассматриваю как возможного кандидата на вынос вовне класса.

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


2.


Для этого в тестируемом коде все это должно быть при надобности завернуто в абстракции и инжектиться в класс через DI (собственно это даже просто один из принципов SOLID — "инверсия зависимости").

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

UFO just landed and posted this here
На любой совет всегда найдется случай когда он неприменим, поэтому любой совет надо воспринимать исходя из этого.

Это да. У меня просто "детская травма" от абсолютистов TDD и юнит-тестов. 8))

UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here

Интересный факт. Как известно, TDD появилась и стала популярной благодаря усилиям Кента Бека, который был РП системы расчета зарплаты в Крайслере. Это история. А факт в том, что эта система была запущена в 1997, а в 1999 были прекращены какие-либо ее доработки, и в 2000 полностью от нее отказались.

https://en.m.wikipedia.org/wiki/Chrysler_Comprehensive_Compensation_System

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

Не долгоживущего это бы ладно. Так он же скорее всего был и неудачен?

Смотря для кого. Кент Бек только выиграл.

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

Если я правильно помню, после запуска Кент вскоре ушел, а новый РП не смог продолжить разработку. То ли доков не хватило, то ли тестов...

На вики так написано:

Chrysler was bought out by Daimler-Benz in 1998, after the merger the company was known as DaimlerChrysler. DaimlerChrysler stopped the C3 project on 1 February 2000.

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

Наверно, они были большими экспертами в XP, раз так быстро увидели бесперспективность проекта? Ну, или у них был огромный собственный департамент, который занимался финансами (читайте про Debis AG на этой странице)? Оставим выводы читателям. Лично я никаких определённых выводов из этой истории сделать не рискну.

UFO just landed and posted this here

Пишут маленькие интеграционные тесты?

Мне кажется, тут нет именно хейтеров юнит-тестов, тут есть согласные с мнением автора статьи. Согласные с тем, что TDD, как его часто понимают («тесты важнее кода») — ещё одна крайность, которая оказывается полезной/применимой в ну очень ограниченном подмножестве случаев.
То есть проблема не в TDD, а в том, что какие-то люди принимают за TDD? «Сама придумала, сама обиделась»? :)

Практически полностью согласен со всеми тезисами автора статьи, прямо как будто сам писал)

Sign up to leave a comment.