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

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

Всегда считал unit-тесты чем-то почти бесполезным. Они легко увеличивают трудозатраты на разработку в полтора и более раз. А долгосрочной пользы от них я не вижу. Другое дело - полноценные интеграционные и системные тесты. А еще лучше посмотреть в сторону ATDD. Да, разработчик и автотесты на кейсах, и сам код пишет/конструктор использует. Увеличивает бюджет в краткосрочной перспективе. В долгосрочной - продукт с минимумом багов, который без проблем лет 5 проработает. Если еще и с продуманной архитектурой - части системы можно изолированно заменять на более современные с некоторой периодичностью - продукт и 50 лет проживет.

Unit-тесты фиксируют работоспособное состояние API.
Без них легко сломать то, что раньше работало, и не заметить этого.

Это делают интеграционные или e2e тесты.

Юнит тесты ничего про АПИ не знают. И позволяют сломать его миллионом разных способов.

Но ведь публичные методы класса - это вполне себе API класса и юниты про него знают

Средний публичный метод класса обычно сам по себе не имеет смысла. Он сходит в 5 других публичных методов и как-то преобразует их результат.

Заложиться на инварианты это хорошо и правильно. Но как вы их проверите юнит тестами? Они в тестирумый метод извне и часто из нескольких мест приходят.

4 минуса и ноль ответов. С чем люди несогласны непонятно.

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

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

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

Где могут быть баги при нарушении инвариантов?

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

Разные методы для одного и того же юзера возвращают разное. (допустим проблема в месте выдающем куки/токены). Нетестируется юнит тестами и работает неожиданным образом.

Добавлен новый тип объектов о котором мы не знаем, или еще хуже убран существующий. Тоже из той БД или хранилища где это все лежит. Нетестируется юнит тестами и работает неожиданным образом.

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

Можно продолжить при должной фантазии.

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

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

API классов: ожидаемое поведение, корректность рассчетов.

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

Как будут выглядеть юнит-тесты для этих функций и причём тут API?

Меня вполне устраивает пара ассертов с проверкой вырожденных случаев и еще пара с проверкой типичных. Доказательством математической корректности в C++/Java заниматься не то чтобы нельзя, да вот неудобно очень)

Я не уверен, то ли самое ваш оппонент пытался ввернуть, но:

> Меня вполне устраивает пара ассертов с проверкой вырожденных случаев и еще пара с проверкой типичных. Доказательством математической корректности в C++/Java заниматься не то чтобы нельзя, да вот неудобно очень)

Тут проблема, что само понятие вырожденных случаев достаточно специфично, и нужна не просто математическая корректность, а корректность при наличных средствах расчётов (которые ограничены и специфичны). Вот несколько примеров из FMM: для уравнения «a*x**2 + b*x + c = 0»:
тест 1: а=1e300, b=2e300, c=1e300. Вычисление через стандартный путь с «d = b*b — 4*a*c» даст INF по дороге.
тест 2: a=1, b=-1e100, c=1.
Рассчитываем на стандартный double. Обычная формула даст x2=0. Ожидаемое — x2=1e-100. Если важна абсолютная погрешность x2, нам нормально. Если относительная или равенство 0, мы пролетели. Авторы рекомендуют вычислять больший корень и затем меньший как «x2 = c/(a*x1)».

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

Если мы имеем дело с т.наз. «экономическими» расчётами, в которых фиксированная точка и четыре операции арифметики (корень уже за пределами обычного), то там точные значения в тестах не просто возможны — они обязательны. (Хм, да, есть ещё всякие усложнения типа расчёта платежа для аннуитета. Но там можно получить проверенную цифру просто бисекцией.)
Ну а сама проверка тут достаточно проста — каждый случай (каждый фиксированный участок прогрессивной шкалы, попали под ограничение налогов или нет) должен быть проверен на 2-3 значениях в пределах соответствующих диапазонов.
Считать же всё это в двоичной плавучке допустимо только в MVP. В продуктине всё это должно быть заменено на что-то соответствующее General Decimal Specification.

> Функция расчёта квадратного корня уравнения.

Писал тут.

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

Если у вас тесты увеличивают трудозатраты, то это ведь не потому, что ты пишете пару строк теста? Всё равно вам надо как-то тестировать написанное. Причина, вероятно, в том, что написанный код автотестировать по какой-либо причине трудно.

А если тестировать трудно, значит, он не очень хорошо написан, и, скорее всего, в нём хромает соответствие single responsibility principle. Для меня ценность TDD в основном в том, что эта методология заставляет писать такой код, который легко тестировать (и не увеличивать трудозатраты). То есть у меня изначально отнимают целый ряд плохих вариантов структуры программы.

Вас ждёт впереди куча удивительных открытий. И то что single responsibility principle это мара, и что какая-то серебряная пуля (кроме многих лет программирования) делает ваш код лучше, и что тесты бесплатны.

Тесты, особенно ковровые, радикально задирают цену рефакторинга, что приводит к тому что проект становится Легаси через два месяца от старта разработки. Тесты склонны обрастать моками, и вместо тестирования реальных объектов/сервисов идёт тестирование влажных фантазий. Хорошие тесты бизнес логики можно написать только поверх хорошей архитектуры. Но невозможно написать хорошую архитектуру через TDD

НЛО прилетело и опубликовало эту надпись здесь
> Если с тестами проект может стать Легаси, то без тестов он уже Легаси.

Скорее он просто непригоден к развитию теми, кто не освоил глубоко его все стороны. С точки зрения бизнеса это обычно значит, что он непригоден к развитию (bus factor слишком мал). Но до легаси тут ещё один важный шаг — чтобы это реализовалось.

> Напоминаю, что тесты вообще не обязательно запускать.

А вот тут возникает принципиальный вопрос — а кому и зачем они тогда нужны?

Да нет, у меня масса самого разного опыта за плечами, если я "перехожу на TDD", это не значит, что я перехожу с "hello, world", ну честно.

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

проект становится Легаси через два месяца от старта разработки.

Это утверждение занятным образом пересекается с известным определением Майкла Фезерса: "To me, legacy code is simply code without tests."

Если для API предусмотрен контракт с версионностью, обратной совместимостью и интеграционными тестами, то на уровне приложения изменения выглядят следующим образом: меняем код -> меняем unit-тесты. И зачем они тогда вообще нужны? Если на уровне бизнес-требований все покрыто тестами, то какая мне разница что там под капотом меняется?

Это разумное соображение, оно пересекается с другой цитатой с MSDN: "after code reviews, smoke testing is the most cost effective method for identifying and fixing defects in software". Это, правда, мнение, доказательств нет.

Собственно, в первой реплике я же сразу упомянул, что "обеспечение качества" для меня тут не на первом месте. На первом месте именно архитектура, заточенная под тестирование, потому что она отсекает массу плохих архитектур.

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

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

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

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

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

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

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

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

Далее, если компонент меняется, то (см. выше) я всё равно его должен протестировать, иначе как мне убедиться, что ничего не сломалось. Поэтому, да, тест тоже придётся подправить. Но я в любом случае буду его тестировать. Тут важна гранулярность, конечно, я не тестирую private-методы, потому что это не API.

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

Если мы говорим об интеграционных или приёмочных тестах -- ну как бы я двумя руками за, понятно, что они нужны. Но наша программа -- это дискретная система с миллионами состояний, проверить которые я не могу. Изменился компонент A, приёмочный тест говорит, что всё ОК. Юнит-тесты ненадёжны, но приёмочные тоже ненадёжны, потому что из миллионов вариантов будет тестироваться меньше процента. Соответственно, я хотя бы должен убедиться, что компонент сам по себе работает (иначе непрофессионально, см. пункт 1, что возвращает к самому началу).

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

На этому пути есть ловушка.

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

Но наша программа -- это дискретная система с миллионами состояний, проверить которые я не могу

Фаззинг тесты не пробовали? С виду должны вам подойти. При должной продолжительности могут неполохо сработать.

 Это все совсем не синонимы. И в разных местам может быть нужно разное.

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

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

Фаззинг тесты не пробовали?

Собираюсь, но пока нет. По-хорошему всё делать надо, конечно :) Тема TDD интересна тем, что она прямо влияет на процесс и архитектуру, а не только решает вопросы на этапе QA.

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

Для одних модулей важнее читаемость. Для других быстрота. hot path и все остальное. Типичный случай.

Все вместе будет читаемым и быстрым там где надо.

Если нетестируемый юнит тестами быстрый код экономит бизнесу миллион долларов в месяц на железе, то это хорошая архитектура или нет?

Все вместе будет читаемым и быстрым там где надо.

Ещё раз, давайте отталкиваться от совместного понимания. Либо вы согласны с мнением выше ("нетестируемая архитектура плоха"), либо нет. Если согласны, то ваша реплика звучит так: "у меня плохая архитектура в проекте везде, но так надо". Хорошо, надо так надо.

Если нетестируемый юнит тестами быстрый код экономит бизнесу миллион долларов в месяц на железе, то это хорошая архитектура или нет?

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

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

Какие же у вас радикальные воззрения.

Возьмем, например, Кликхаус. Громкий, успешный, российский проект. 2 миллиарда долларов говорят оценка. Вот его код https://github.com/ClickHouse/ClickHouse попробуйте там найти классические юнит тесты. И посмотреть сколько кода ими покрыто.

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

Вы считаете что у Кликхауса "бизнес держится на системе, состоящей из непротестированных компонентов без формализованных примеров входа-выхода, и пока это работает" и вы бы все передалали?

Смотрите на мир шире. Юнит тесты это конкретная методика. Далеко не везде применимая и необходимая. Не надо фанатизма, надо больше гибкости.

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

Вопрос о том, держится ли бизнес Кликхауса на системе, состоящей из непротестированных компонентов, не является предметом мнения. Это фактический вопрос, и ответ на него либо "да", либо "нет". Причём "нет" ещё не трагедия -- ну вот так, и что? Живём же.

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

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

Я не считаю все виды таких архитектур плохими. Вот даже показательный пример нашел. На ваш взляд ахритектура Киликхауса плохая. На мой она хорошая. Потому что она обладает важными свойствами: скорость, понятность, расширяемость, разумное количество ошибок. А возможность написать юнит тесты не так важна.

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

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

Я давно перешел в продуктовые компании. Там заказчик это растяжимое понятие. В среднем все сами себе заказчики. Если дело касается технических решений, а не бизнеса. В рамках держаться надо, но в общем свобода полная. Сам же будешь по ночам дежурить со своим кодом. И лучше бы ему не падать.

То что вы называете архитектуру обладающую любыми свойствами кроме "можно написать юнит тесты" плохой 

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

А возможность написать юнит тесты не так важна.

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

Не бывает нетестируемого кода. Бывают такие себе тестовые фреймворки.

Это вопрос цены и целесообразности. Выше же написали:

Тесты склонны обрастать моками, и вместо тестирования реальных объектов/сервисов идёт тестирование влажных фантазий.

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

> Вы считаете что у Кликхауса «бизнес держится на системе, состоящей из непротестированных компонентов без формализованных примеров входа-выхода, и пока это работает» и вы бы все передалали?

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

> Смотрите на мир шире. Юнит тесты это конкретная методика. Далеко не везде применимая и необходимая.

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

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

Но это мне, ненастоящему программисту, над которым не стоит менеджер с KPI в руках, в котором прописан «процент покрытия тестами», так можно.

Формальные unit-тесты IMHO оправданы там, где они проверяют код, который с немалой вероятностью будет меняться — т.е. один и тот же ранее написанный тест будет проверять разные реализации. Такое, своего рода, повторное использование кода.
Я вот тут для будущей своей статьи повозился с заменой реализации ряда методов, написанных другим человеком — и мысленно возблагодарил этого человека, за то, что он не поленился тесты написать.
PS Неплохо бы ещё всегда понимать, какой код будет меняться, а какой — нет.
Но тут — как получится.

Ну, я предлагаю разделить всю эту тему на три разных вопроса: 1) что даёт TDD; 2) зачем формальные тесты и 3) что вообще тестировать.

"Не тестируем тривиальные вещи" или "не тестируем GUI/CSS/одноразовый код" -- это про (3). Пункт (2) сразу про две вещи: код (на уровне компонентов) гарантированно работает как заявлено в соответствии с указанным входом-выходом и про некую "живую" документацию, позволяющую понять, как этим кодом можно пользоваться, что важно для меня будущего или других участников, но неактуально для кода вида "написал и забыл". А вот (1) ещё про то, что вы на выходе получаете архитектуру, обладающая рядом свойств, среди которых есть "тестируемость".

Я написал чисто ответ на процитированный фрагмент: что тестировать можно и без излишнего формализма.
И — с позиции простого наемного разработчика: какую архитектуру с какими там свойствами получает менеджер нанимателя — это вопрос не разработчика: затраты на сопровождение на получаемые им деньги влияют крайне опосредованно.
Что касается формальных тестов как документации — это весьма неполная документация: это — всего лишь набор примеров использования. Насколько он полезен? Это зависит от мнго чего разного. Для настоящего программиста «текст программы все объясняет», да и для ненастоящих — тоже служит подспорьем. А полноценная документация — она куда лучше — но куда дороже обходится. Так что тут имеем компромис между трудоемкостью документирования и пользой от документации. В некоторых условиях формальные тесты могут быть решением этого компромиса. Но тут надо в каждом конкретном случае смотреть и думать IMHO.

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

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

Соответственно, юнит тесты позволяют

  1. Протестировать какую-то функциональность "глубоко внутри" системы.

    Вы пишете модуль, который расположен где-то очень глубоко, его ввод/вывод проходит на кучу слоев и влияет на кучу всего. Протестировать такой модуль "извне" будет очень нетривиально. А выкатывать непротестированный код? Откуда вы знаете, что он работает, хотя бы в тех условиях, которые вы предусмотрели?

  2. Протестировать негативные сценарии.

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

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

  4. Определить и фиксировать контракт модуля

При этом надо подходить без фанатизма и понимать, что

  1. Архитектура важнее

  2. Надо правильно выбирать, что есть "юнит" при тестировании, это не обязательно самая маленькая возможная единица

  3. Надо минимизировать моки. Особенно, минимизировать моки, которые "влезают внутрь" через рефлексию (или подмену символов на линковке).

  4. Иногда юниты не целесообразны

  5. Без фанатизма

> Всегда считал unit-тесты чем-то почти бесполезным. Они легко увеличивают трудозатраты на разработку в полтора и более раз.

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

> А еще лучше посмотреть в сторону ATDD.

Ну так может это внутренний аналог ATDD.
НЛО прилетело и опубликовало эту надпись здесь
> Для этого давно придумали типы.

1. Осталось «давно» всюду завезти эти типы. Я в курсе, что вы работаете с языками, где такое штатно, но >99% работ такого не позволяют.

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

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


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


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

>Вы так пишете, как будто это что-то плохое.
Заказчику как правило почему-то нужен работающий код, а не тестируемый. А то, что вы написали код тестируемый, или скажем соответствующий SOLID — а это кому-то нужно, кроме вас?

Я согласен с автором в том, что цели надо ставить правильно.

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

Именно неверные цели — плохо. Цель — получить надежный сопровождаемый код, с приемлемым числом багов, за разумные деньги (именно поэтому и 100% покрытие обычно плохо — потому что повышает стоимость разработки, почти не увеличивая надежность по сравнению с покрытием скажем 20% ключевого или сложного кода).

а разве тестируемый код (или соответствующий SOLID) обязательно должен быть нерабочим?

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

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

Если цель тестирование и SOLID

Интересные у вас цели.
Цель — работающий код, который выполняет поставленную задачу.


Тесты — средство убедиться и гарантировать что код работает. Не единственное, но лучшее.


С точки зрения рационального использования бюджета [...] покрываем тестами то что берём в долгосрочный контракт

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


Авось не сломается.
и тааак сойдёт

> Интересные у вас цели.

Очень странно читаете. Это не у него цели, это «если» цели такие спущены сверху.

> Тесты — средство убедиться и гарантировать что код работает. Не единственное, но лучшее.

Нет, это главное заблуждение во всей тематике.

Тесты ничего не гарантируют. Стандартный пример: функция умножения. У вас тесты, что 1*1==1, 2*2==4. В функции написана обработка двух случаев: x==y==1 и x==y==2, иначе она выдаёт -42. Тесты прошли? Да. Есть гарантия работоспособности? Фиг без масла.

Более-менее гарантию даёт верификация, начиная от визуальной (код прочёл глазами автор, коллега или ревьюер/аудитор откуда-то ещё), что код выполняет то, что нужно, и вплоть до математической, которую рекламирует (заслуженно) коллега 0xd34df00d и которая недоступна 99% разработчикам. А вот тесты обеспечивают:
1) Защиту от типовых проблем чтения кода (когда человеку тяжело держать в голове все особенности — вспомним, например, правила обращения с типами в выражениях C/C++).
2) Обеспечение правильного понимания подложки (например, что мы правильно поняли, какую именно длину меряет strlen(), что она не в символах и не в кодовых пунктах).
3) Демонстрацию принципиальной работоспособности для непосредственного заказчика (хоть ближайшего ПМ).
И вот (1) + (2) дают условия для доверия верификации (повторюсь, начиная с визуальной).

Насчёт «убедиться» почти согласен. «Почти» — потому что 100% они всё равно не дадут, но дадут какой-то приемлемый уровень.
«Лучшее» — нет, ни в коем случае. Это вспомогательное средство для верификации в контролируемых условиях, ну и (3) — средство отчётности.
НЛО прилетело и опубликовало эту надпись здесь
Просто на будущее, немного оффтопа, а как это вообще сделать?
НЛО прилетело и опубликовало эту надпись здесь
если стейтмашина, описанная на этом языке, приходит в состояние со свойством P, то она обязана была пройти через состояние со свойством P'. Как это делать тестами, я не знаю вообще, совсем, как класс

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

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

Ну так это совершенно другая задача

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

И я о том же.


Задача: при установке параметра N стейт-машина должна пройти через состояние X, и прийти в состояние Y.
Решение: выбрасываем событие при установке каждого состояния, в тесте подписываемся. Устанавливаем параметр N, запускаем стейт машину. Собираем цепочку состояний в массив, проверяем что в массиве есть состояние X.


Реализовав наблюдатель, можно записать эталонные цепочки состояний в датасет (Параметр: состояние A, состояние B; N: x,y; A: b,c;), зафиксировав корректное поведение стейт машины, и дальше проверять полностью всю цепочку на соответствие, чтобы в один день чего-нибудь не сломалось.


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

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

либо я вас сильно не понимаю (что вы делаете-то, ну, в терминах реального мира?), либо вы сильно усложняете, потому как кажется, что всё это раскладывается довольно просто


для любой последовательности событий {s_i}

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


если в ней есть событие s_m такое, что P(s_m)

поиск в массиве, и сравнение значения с результатом функции


то в ней обязано быть событие s_n, n < m

поиск в массиве


такое, что P'(s_n)

обычный if

> то можно нагенерировать все возможные последовательности

Вот в этом и ключевое. Если вам нужно _доказать_, что нагенерированы все возможные последовательности, автоматически (то есть тест на код, где соответствующее состояние обойдено в каком-то очень частном случае типа param123=-123456789, упадёт), как вы это сделаете?
Я не вижу тут путь без анализа собственно графа состояний и переходов автомата.

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


ну вот стейт машина аудиоплеера.
у него есть состояния "пауза", "воспроизведение", "стоп". итого 3.


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


а что у вас за стейт-машина, для которой это проблема?

НЛО прилетело и опубликовало эту надпись здесь
> а что у вас за стейт-машина, для которой это проблема?

У меня SIP протокол. Это 4-уровневая конструкция с машиной состояний на каждом уровне. Все примеры приводить не буду, но, например, такое:
Есть звонок A->B через прокси (точнее, b2bua) X. B заявляет слепой трансфер на C. X исполняет этот трансфер. B временно отсоединяется, система инициирует звонок на C. В это время инициируется keepalive в сторону B. B не поддерживает UPDATE, отправляется re-INVITE, для которого возможно, что другая сторона в одностороннем порядке меняет кодеки. B таки отвечает сообщением со сменой кодеков. Звонок на C не состоялся, идёт восстановление звонка A<->B. Теперь надо ухитриться передоговорить A с B на общий набор кодеков. Включается пересогласование с некоторым возможно общим набором кодеков в оффере… но оказывается, что A — тоже прокси, которое испытало такую же проблему и запускает свой автомат пересогласования. A отвечает «вы не вовремя» и надо подождать (точнее, они друг другу такое заявляют). Теперь вопрос, у кого генератор случайных чисел даст меньшее время, чтобы пойти на вторую попытку первым:) и в это время должно храниться как факт что у нас есть задержанное пересогласование, так и атрибуты с которыми оно допустимо. Осложнением работает то, что A и B согласовались на вроде бы одинаковые кодеки, но на самом деле конфликтующие annexʼы кодеков, и требуется ещё один круг с исключением этих кодеков.
И как вишенка на тортике — связь с B по TCP порвалась, инициация с нашей стороны из-за NAT невозможна, и исходящие запросы откладываются до восстановления.
Если попытаться это описать в машине состояний, обнаружится, что сложность уже давно зашкалила за возможности укладки в голове не только среднего, но и ведущего программиста;\\ и решить можно только тщательно проработав схему в виде чистых состояний на двух уровнях, ну а саму эту схему желательно держать в верифицируемом виде (увы, пока не сподвиглись).

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


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


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

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

Коды состояния в SIP есть, и похожи на HTTPʼшные. Например, отказ принять re-INVITE из-за того, что сам начал переговоры подобного рода, это 491.
Но это только пара процентов от необходимого.

Я и не требовал от вас осмысления SIP как предметной области — её вон можно осваивать 20 лет и не освоить. Я привёл реальный пример сложной ситуации, которая укладывается на машину состояний с таким набором состояний и переходов между ними, что человеку сложно осилить эту сложность. И это только одна из, наверно, 10 таких машин на разных уровнях.

> В SIP есть свой

Даже на уровне протокола это малая часть проблем. Проблемы там другие и посложнее — например, одно только различие ACK-to-pos и ACK-to-neg на уровне диалога способно, как оказалось, сломать мозг не одному сотруднику… но, опять же, я не об этом и не прошу вдумываться.

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

Что должны обозначать эти коды?
Например, отказ принять re-INVITE из-за того, что сам начал переговоры подобного рода, это 491.
Но это только пара процентов от необходимого.

В моём представлении, эти коды должны обозначать остальные 98%

> В моём представлении, эти коды должны обозначать остальные 98%

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

Если "снаружи" вы узнаете больше информации о "внутренних" проблемах конкретного участника (A узнает о внутренней проблеме B, и благодаря этой информации система сможет принять более разумное решение о том, как с ним взаимодействовать), это поможет в решении проблемы?


Если да — то завести новые коды на подобные случаи, отдавать их наружу. Тестировать это не будет проблемой (если код ответа 1000 -> делать одно, если код ответа 1001 -> делать другое).


Если нет — тогда я вообще не понимаю вас, врядли в этих условиях стоит продолжать: я вам не собеседник :)

> это поможет в решении проблемы?

Нет.

> я вам не собеседник :)

Я повторю ещё раз, что моей целью было показать наличие конкретного примера, когда автомат становится сложнее уровня понимания программиста. В том, что этот пример за пределами понимания постороннего, я и не сомневался и не хотел предметного анализа ситуации.
НЛО прилетело и опубликовало эту надпись здесь
> Там в том числе нужно убедиться, что если стейтмашина, описанная на этом языке, приходит в состояние со свойством P, то она обязана была пройти через состояние со свойством P'.

Если именно «обязана была», то видится что-то такого рода:
1) Описание машины состояний в в виде списка состояний и ссылок на функции перехода хранится в отдельном файле в машинно-читаемом виде (чтобы и для человека, предложим JSON/YAML/etc.). Метод, в котором реализуется таблица перехода, генерируется из этого описания в процессе компиляции.
2) Анализатор этого описания проверяет известными средствами все пути.

Вы там ссылались на Agda — я подозреваю, что они по сути делают то же самое, но средства встроены в язык.
НЛО прилетело и опубликовало эту надпись здесь
> Какими известными, если путей бесконечное число?

Например, по вашим обозначениям, есть пути:
Pʼ->A
A->B
B->A
B->P
и других переходов в A, B, P нет.

да, оно может бесконечно бегать по кругу A->B->A. Но если оно добралось до P, то оно прошло через Pʼ.

Понятно, что существует 100500 вариантов усложнения этого (хотя бы параметризация), но основная часть — простые случаи, которые решаются. Математика для этого отработана очень давно.
НЛО прилетело и опубликовало эту надпись здесь

Фаззить на все деньги. Математически гарантии нет, но на практике подойдет для любого практического применения.

> Этого условия нет.

То есть переходы есть? А как тогда вообще можно что-то доказать?
Тесты ничего не гарантируют. Стандартный пример: функция умножения. У вас тесты, что 11==1, 22==4. В функции написана обработка двух случаев: x==y==1 и x==y==2, иначе она выдаёт -42. Тесты прошли? Да. Есть гарантия работоспособности? Фиг без масла.

Высасывание из пальца, даже не хочу всерьёз обсуждать


Более-менее гарантию даёт верификация, начиная от визуальной

Она несёт в себе куда больше рисков просто из-за человекофактора


100% они всё равно не дадут, но дадут какой-то приемлемый уровень

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

> Высасывание из пальца, даже не хочу всерьёз обсуждать

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

> Она несёт в себе куда больше рисков просто из-за человекофактора

Нет, не больше. Основные проблемы человекофактора как раз подпираются тестами, но тесты без верификации невалидны.

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

Не будет, пока не будет гарантия, что тесты в принципе проверяют то, что нужно, а не что-то левое.
Не будет, пока не будет гарантия, что тесты в принципе проверяют то, что нужно, а не что-то левое

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


Почему вы изначально исходите из ситуации, что тест проверяет "что-то левое"? По-моему очевидно, что вероятность "визуально наверефицировать" что-то левое сильно выше.

> Почему вы доверяете разработчику, который «визуально верифицирует» код, но не доверяете тому, который пишет тесты?

Я не доверяю на 100% обоим. И основной код, и тесты должны проходить пир-ревью.

> Почему вы изначально исходите из ситуации, что тест проверяет «что-то левое»?

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

Поэтому повторяю: тесты проверять надо так же, как основной код.

> По-моему очевидно, что вероятность «визуально наверефицировать» что-то левое сильно выше.

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

Как-то был роскошный пример на это — коллега написал, на Java, a.reverse(), где a — int, а результаты не сходятся… оказалось, что reverse() это reverseBits() по сути, а им нужен был reverseBytes(). Он тупил, наверно, час и привлёк ещё народ из отдела… только когда решили на всякий случай заглянуть в доку (а он возвращает изменённое значение или правит на месте?), увидели ключевое слово.
Вот это как раз то, для чего нужны тесты. А что он вообще вызывается, а не передано 1:1 или например сдвинуто на 5 битов влево — это на верификацию.
Нет, не обязательно конечно. Тесты — они ловят баги (хотя и не всегда). И применение принципов SOLID обычно дает код лучше сопровождаемый, чем если делать без них. И как средство документирования тесты тоже имеют место быть. И в рефакторинге помогают понять, не сломали ли мы код в процесе. Популярные инструменты обычно работают, лучше или хуже, они же не просто так стали популярны.

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

А вы ему говорите — не, погоди, мы еще на 100% тестами не покрыли…
Тесты — они ловят баги (хотя и не всегда)

Тесты не ловят баги, а фиксируют поведение программы на определённых входных данных. И да, хорошие тесты — это документация.

>хорошие тесты — это документация
Так я и не спорил с этим.

С другой стороны, а вот зачем вам документация? :) Точнее, документация нужна, чтобы что?

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

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

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

Да и заказчик всегда может нас обвинить в чем угодно. Но если он хочет выпустить продукт завтра, и явно это озвучит — то все что сломается, уже будет его проблемой, а не нашей. Сломается — починим, в точном соответствии с TLA.
Заказчику как правило почему-то нужен работающий код, а не тестируемый. А то, что вы написали код тестируемый, или скажем соответствующий SOLID — а это кому-то нужно, кроме вас?

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

>Если после сдачи проекта заказчику хоть трава не расти"
Ну, да. Но опять же — заказчику от вас нужно сопровождение (или не нужно). Поэтому цель — оно, а не те средства, которыми вы его обеспечите (вам конечно может быть не все равно). Скажем, почему покрывать критичные части сейчас, а не тогда, когда баг найдем (а если не найдем — то никогда)?
НЛО прилетело и опубликовало эту надпись здесь

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

На своем опыте поделюсь кейсами: в проекте 7 разрабов, есть фичи/API/компоненты и модули которые все юзают. Однажды, на ревью чел выкатывает pr, в котором удалил одну из строчек. Попросив его запустить юнит-тесты, результат: упало два, моя фича сломана, ну и какого черта такое произошло? Как оказалось, удалил случайно. Вывод: в общей команде, когда каждый день правки и фиксы, тесты являются ГАРАНТОМ сохранности фичей.

Далее: фича по работе с картой, нужно формировать запрос к бд чтобы на карте отобразилось то, что соответствует запросу. Значит входных параметров для запроса 4, в общем вариантов для параметров получается >30, и что, мне теперь написав код, запускать прогу и руками смотреть что все корректно работает+в голове держать и помнить все нюансы и варианты? СЕРЬЕЗНО? Учитывая, что с каждым часом я могу поменять структуру кода и придется заново проверять что все варианты работают. Нет, спасибо, итого пишу тест на строку формирования запроса(31 тест) за несколько млск отработал и показал что все работает, значит код готов, прогу один раз запустила просто полюбоваться. И можно код менять, т к тесты гарантия что все ок + в команде над этой фичой есть ещё разраб. Захотел он поменять мой код, поменял. Я спрашиваю, а тесты ты запускал? Нет, зачем и так все работает? Серьезно?! Запусти-ка! Упали 😅 Тесты не трогай, код в порядок приведи.

И третий кейс: тестировщик нашел баг, мне что теперь его воспроизводить руками как он? Пошла, тест к текущим написала, ну да красный, ща поправлю, поправила, все тесты запустила, все зелёные, ничего не сломалось, збс! А ещё, иногда от сервера нужно подставить ответ и посмотреть что все ок, тогда мок ответа в тест и расслабляешься, все ок (без всяких чарлизов, дебагеров в рантайме и без сетевых задержек). Ещё я люблю писать тесты контрактов чтобы, например, если появился новый формат даты, добавить его и тесту дать эти форматы - значит зафиксировано 💯

Итого: TDD - сохраняет и гарантирует что все работает, сохраняет фичи при правках(регресс) и самое важное для разраба - автоматизирует его время разработки ❤️ это был крик души, потому что дедлайны жёсткие, качество требуют, спасаюсь как могу TDD. Моя любимая цитата: Good developers write good code, and great developers test their good code.

> Однажды, на ревью чел выкатывает pr, в котором удалил одну из строчек. Попросив его запустить юнит-тесты, результат: упало два, моя фича сломана, ну и какого черта такое произошло? Как оказалось, удалил случайно. Вывод: в общей команде, когда каждый день правки и фиксы, тесты являются ГАРАНТОМ сохранности фичей.

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

А ещё у вас явно нет автоматического CI, иначе бы тот pull request немедленно получил бы запрет от CI-интеграции и его не имело бы смысла показывать, пока не пройдут тесты (или, в особых случаях, не будут отменены для этого коммита).

> Итого: TDD — сохраняет и гарантирует что все работает

Нет, итог — тестирование «сохраняет и гарантирует что все работает», а не TDD по Беку.

Пару месяцев назад на DOU был чат про TDD. Результат в общем тот же — больше половины, на практике, под TDD понимают просто использование тестов.
«Кастанеда писал совсем о другом!» ([ГрОб])

И да, привинтите таки автотесты к своим pull requests, иначе очень скоро кто-нибудь плюнет на ваши «сначала запусти юнит-тесты», а вы будете потом расчищать авгиевы конюшни.

Спасибо за комментарий, учту ваши пожелания.

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

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

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

> а после написания тестов быть уверенной что моя новая фича готова и ее никто не сломает(плюс тестов).

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

Ну да, ещё остаётся вопрос, насколько осмысленен сам тест — тут была рядом ветка на эту тему. Но тут есть ещё одна проблема: проверять надо не только когда тест пишется, но и регулярно — после. Я поэтому предполагаю или инверсии тестов, или рандомизированные мутации.

Я вам по секрету скажу: тесты можно писать и без TDD. Вы только никому не говорите.

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

In controversia veritas nascitur. Спасибо всем за пищу для размышлений)

Сейчас много букв будет.

Честно попытался освоить TDD, читая книгу Python: разработка на основе тестирования. И мне, конечно, на захотелось, как мартышке, делать пример to-do листа из примера в книге. Придумал свой простенький пет-проект -- цитатник. Случайное отображение цитат. И буквально на ранних этапах захотелось проверить, что из базы извлекается действительно СЛУЧАЙНАЯ цитата. Казалось бы, по методике TDD не надо тестировать внешние зависимости. Пишу на Django: dq_next = TbDictumAndQuotes.objects.exclude(id=dq.id).order_by('?').first()и база должна отдать случайную запись. Все! ДОЛЖНА!! Но а вдруг не случайную? Опять же, я только изучаю TDD и почему бы не проверить, тем более, что кейс написания такого теста действительно интересный.

Как проверить случайность? Сделать несколько запросов (например, раз в десять больше чем всего записей в базе), построить распределение и проверить, что ответы возвращаются более менее равномерно (например, каждая запись в распределении будет выводиться от восьми до двенадцати раз). Сказано -- сделано. Все ок. Тест, правда, получился громадный (раз в триста больше чем та одна строчка которую я хочу протестировать). Разворачиваю в продакшн. И там я сразу, глазами. вижу, что распределение не случайно. Тест тоже это показал, но самое главное, я и без него увидел. Почему нет случайности? А вот на это TDD ответа не даёт. То ли файл базы (использовал SQLite) как-то не так кэшируется в файловой системе провайдера, толь кэшируется SQL запрос, толи где-то что-то кэшируется на уровне CGI, то ли ещё что-то... Что же получается? Я оттестировал то, что не должен; ответа отчего и на каком уровне не работает не получил; потратил кучу времени на написание теста; и при этом некорректную работу распределения увидел ещё до запуска теста, ГЛАЗКАМИ! И кому это нужно?

Конечно, я понимаю, что для сложных проектов, которые пишет много людей TDD поможет локализовать проблему до уровня модуля. Но не все же будут писать тесты того что тестировать не нужно?! А значит, в настоящем, боевом проекте не факт, что удастся локализовать проблемный модуль. И тогда возникает вопрос "зачем"? Если я при написании теста должен подумать о всяких "переполнениях буферов", "переходах через ноль", "некорректных данных", не проще ли это сразу написать и проверить в самом коде, и не надо тестов? "Проверки на дурака", обложить всё исключениями, проверить данные -- хороший тон. Это рекомендовалось делать ещe до того как TDD стал майнстримом!

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

P.S. К слову, я имею свойство ломать системы. Порой, буквально нескольким кликами делаю что-то, что выводит из строя что-то, что без проблем работало годами. Само собой, пока сам напишу модуль или систему, наступлю на все грабли. С некоторых пор предпочитаю делать даже пост-проверку, что транзакция записи в несколько баз банных прошла успешно (например, периодически запуская отдельный процесс проверяющий целостность обработанных записей, которые не помечены как проверенные). Мне кажется, что таким как я TDD -- противопоказан. Т.к. рано или поздно внутренняя паранойя заставит писать тесты тестов, тесты тестов для тестов и так до бесконечности).

P.P.S. Если интересно, что получилось со случайными цитатами, то вот. Цитаты не появляются случайно. Цепочка "случайных" цитат закольцовывается. До сих пор не пойму почему. Я даже переписал order_by('?') на совсем не оптимальный dq_next = TbDictumAndQuotes.objects.get(id=int(random.uniform(0, TbDictumAndQuotes.objects.exclude(id=dq.id).count)))

Первое: если вы объединили тестирование кода бэкенда с тестированием БД - это не юнит тест. В зависимости от условий это может быть системный тест или e2e...

Второе: рандомная выборка из БД - отдельная тема, с которой следует внимательно разобраться, но к TDD это уже не имеет прямого отношения. (В первых же ссылка в поиске есть несколько рецептов... И проверять генератор надо на самой БД)

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

Вывод: Вы недооценили TDD, потому что решали не ту проблему и не тем способом.

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

Публикации

Истории