Эта статья является переводом материала «TDD: What went wrong or did it?».

В сфере разработки программного обеспечения уже давно хвалят Test Driven Development (TDD, разработка через тестирование). Однако в последнее время было сказано много резких слов в адрес TDD, поскольку его обвиняют в плохом проектировании программного обеспечения и невыполнении многих своих обещаний. Кульминацией этой тенденции стал пост Дэвида Хайнемайера Ханссона «TDD is dead. Long live testing.» (TDD мертв. Да здравствует тестирование).

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

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

TDD это не «Проектирование через тестирование»

TDD расшифровывается как “Разработка через тестирование”. К сожалению, многие неверно истолковывают это как “Проектирование, основанное на тестировании”. Эта неточность может показаться невинной, но поверьте мне, это не так. Позвольте мне объяснить.

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

Если вы разрабатываете в первую очередь для тестируемости, вы получаете то, за что платите, — тестируемый код. Чаще всего этот дизайн будет полностью не связан с бизнес-областью и требованиями проекта. Он будет напоминать огромный граф объектов, полный случайных сложностей... но он будет проверяемым. Тестируемый тестами, которые тонут в моках (имеется в виду mock как тестовый двойник), и полностью сломается после изменения одного кусочка в реализации. Это то, что называется “повреждением, вызванным тестом”, и это ярко показано в блоге Дэвида Хайнемайера Ханссона «TDD is dead. Long live testing.»:

Нынешний фанатичный опыт TDD приводит к тому, что основное внимание уделяется модульным тестам, потому что это тесты, способные управлять дизайном кода (первоначальное обоснование для test-first – сначала тестирование, потом реализация). Я не думаю, что это здорово. Test-first приводят к чрезмерно сложной сети промежуточных объектов и косвенных обращений, чтобы избежать «медленных» действий. Например, попасть в базу данных. Или файл IO. Или пройти через браузер, чтобы протестировать всю систему. Это породило некоторые поистине ужасные архитектурные уродства. Густые джунгли служебных объектов, командных шаблонов и прочего.

Как должно быть? Ваш бизнес-домен должен определять ваши решения по проектированию. Выберите реализацию, которая наилучшим образом соответствует проблеме, которую вы пытаетесь решить. Нет смысла в полноценной модели домена, если все, что вам нужно, - это обычный интерфейс CRUD - вместо этого реализуйте шаблон Active Record. Если все, что вам нужно, это сценарий ETL, используйте шаблон Transaction Script.

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

TDD это не (только) о модульных тестах

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

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

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

  1. Вы имеете дело со сложной бизнес-логикой? Вам действительно нужны модульные тесты здесь.

  2. Вы выполняете только простые операции CRUD? Используйте интеграционные тесты или сквозные тесты.

  3. Сценарий ETL? Достаточно сквозных тестов.

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

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

Unit != Class

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

Определение модуля, которое мне нравится больше всего, принадлежит Рою Ошерову, автору книги The Art of Unit Testing:

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

Единица работы (a unit of work) - это единый логический функциональный вариант использования (use case) в системе, который может быть вызван некоторым общедоступным интерфейсом (в большинстве случаев). Единица работы может охватывать один метод, целый класс или несколько классов, работающих вместе для достижения одной логической цели, которую можно проверить.

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

Отсутствие буквы D в TDD

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

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

  • Как вы можете идентифицировать проверяемый код? Легко - по тому, есть тесты или нет.

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

Поэтому, даже если реализация поддается тестированию, она все равно может не соответствовать решаемой проблеме и бизнес-области. Следовательно, отсутствующая буква “D” в TDD является “Доменом” бизнеса/проблемы. Вот почему я считаю, что DDD является необходимым условием для разработки на основе тестирования. Методология DDD применима не только к сложным моделям предметной области - напротив, она определяет набор рекомендаций по выбору наилучшего инструмента для работы в соответствии с проблемной областью. Но это тема для совершенно другой статьи.

P.S TDD 2.0

TDD был «заново открыт» Кентом Беком более десяти лет назад. Возможно, пора снова открыть TDD. Помимо модульных тестов, новая спецификация должна касаться других типов автоматизированных тестов, которые в то время были недоступны. И, конечно же, вместо того, чтобы работать против, TDD должен тесно сотрудничать с бизнес-областью.

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


  1. dzm
    31.10.2021 10:38
    +5

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


    1. Reformat
      31.10.2021 13:31
      +11

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


      1. BugM
        31.10.2021 17:23
        +4

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

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


        1. dopusteam
          31.10.2021 19:51
          +6

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


          1. BugM
            31.10.2021 21:10
            -5

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

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


            1. BugM
              01.11.2021 23:29
              +2

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

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

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

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

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

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

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

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

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

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

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


      1. 0xd34df00d
        31.10.2021 19:58
        +1

        Что именно вы проверяет тестами в API? Количество аргументов и их типы, или что-то сложнее?


        1. Reformat
          31.10.2021 21:16
          +3

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


          1. 0xd34df00d
            01.11.2021 01:40
            +3

            О, про корректность расчётов интересно. В частности, интересно два случая:


            1. Функция для расчёта налогов по зарплате. Ну там, надо учесть прогрессивность шкалы, ограниченность сверху некоторых налогов, и так далее.
            2. Функция расчёта квадратного корня уравнения.

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


            1. Reformat
              01.11.2021 12:49
              +4

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

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


              1. netch80
                02.11.2021 10:19

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

                > Меня вполне устраивает пара ассертов с проверкой вырожденных случаев и еще пара с проверкой типичных. Доказательством математической корректности в 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)».

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


            1. netch80
              02.11.2021 10:24

              > Функция для расчёта налогов по зарплате. Ну там, надо учесть прогрессивность шкалы, ограниченность сверху некоторых налогов, и так далее.

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

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

              Писал тут.


    1. rg_software
      31.10.2021 14:31
      +5

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

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

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


      1. TerraV
        31.10.2021 14:49
        +4

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

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


        1. fzn7
          31.10.2021 15:54
          +2

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


          1. netch80
            01.11.2021 12:28
            -1

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

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

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

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


        1. rg_software
          31.10.2021 15:58
          +4

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

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

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

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


          1. dzm
            31.10.2021 16:09

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


            1. rg_software
              31.10.2021 16:20
              +2

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

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

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

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


              1. TerraV
                31.10.2021 17:12
                -1

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

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

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


                1. rg_software
                  31.10.2021 17:24
                  +4

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

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

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

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

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

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

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


                  1. BugM
                    31.10.2021 17:55
                    +1

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

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

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

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


                    1. rg_software
                      31.10.2021 19:00

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

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

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

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

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


                      1. BugM
                        31.10.2021 19:19
                        +1

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

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

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

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


                      1. rg_software
                        31.10.2021 19:33

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

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

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

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

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


                      1. BugM
                        31.10.2021 21:06
                        +2

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

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

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

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

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


                      1. rg_software
                        31.10.2021 21:37

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

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

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


                      1. BugM
                        31.10.2021 21:44
                        +2

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

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

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

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

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


                      1. rg_software
                        01.11.2021 05:24
                        +2

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

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

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

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


                      1. nin-jin
                        01.11.2021 06:14
                        -1

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


                      1. rg_software
                        01.11.2021 07:11

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

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

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


                      1. netch80
                        01.11.2021 00:56
                        +1

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

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

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

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


                  1. TerraV
                    31.10.2021 18:24

                    del


      1. mvv-rus
        01.11.2021 07:01

        Всё равно вам надо как-то тестировать написанное.

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

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

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


        1. rg_software
          01.11.2021 07:17

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

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


          1. mvv-rus
            01.11.2021 07:37

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


    1. Alex_ME
      31.10.2021 22:22
      +5

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


    1. netch80
      01.11.2021 00:51

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

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

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

      Ну так может это внутренний аналог ATDD.


      1. 0xd34df00d
        01.11.2021 01:42
        +2

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

        Для этого давно придумали типы.


        1. netch80
          01.11.2021 09:48

          > Для этого давно придумали типы.

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

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



  1. DistortNeo
    31.10.2021 11:27
    +3

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

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


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


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


    1. sshikov
      31.10.2021 14:02
      +5

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

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

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

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


      1. the_toster
        31.10.2021 14:25
        +3

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


        1. TerraV
          31.10.2021 14:32
          +6

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

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


          1. laatoo
            01.11.2021 04:32

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

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


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


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

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


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


            1. netch80
              01.11.2021 10:09
              +5

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

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

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

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

              Тесты ничего не гарантируют. Стандартный пример: функция умножения. У вас тесты, что 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) — средство отчётности.


              1. 0xd34df00d
                01.11.2021 10:31
                +2

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

                Тоже очень люблю этот пример, но есть и более интересные.


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


                Как это делать тестами, я не знаю вообще, совсем, как класс. Очень было бы интересно услышать от знатоков юнит-тестирования, как это можно сделать.


                1. Zaphkiel
                  01.11.2021 12:26

                  Просто на будущее, немного оффтопа, а как это вообще сделать?


                  1. 0xd34df00d
                    01.11.2021 18:14
                    +1

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


                1. laatoo
                  02.11.2021 06:31

                  если стейтмашина, описанная на этом языке, приходит в состояние со свойством P, то она обязана была пройти через состояние со свойством P'. Как это делать тестами, я не знаю вообще, совсем, как класс

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


                  1. 0xd34df00d
                    02.11.2021 06:36
                    +1

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


                    1. laatoo
                      02.11.2021 06:44

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


                      1. 0xd34df00d
                        02.11.2021 06:49
                        +1

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


                      1. laatoo
                        02.11.2021 06:56

                        И я о том же.


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


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


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


                      1. 0xd34df00d
                        02.11.2021 08:10
                        +1

                        Нет, задача более общая. Нужно проверить, что выполняется следующее утверждение: для любой последовательности событий {s_i}, если в ней есть событие s_m такое, что P(s_m), то в ней обязано быть событие s_n, n < m такое, что P'(s_n).


                      1. laatoo
                        02.11.2021 09:33

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


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

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


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

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


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

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


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

                        обычный if


                      1. netch80
                        02.11.2021 10:33

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

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


                      1. laatoo
                        02.11.2021 10:45

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


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


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


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


                      1. 0xd34df00d
                        02.11.2021 11:13
                        +1

                        Это язык для описания смарт-контрактов. Например, простейший, канонический пример контракта — штука, в которую можно закинуть деньги, и которая их перечислит на заранее установленный адрес, если M из N заранее оговорённых людей выразило своё согласие.


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


                      1. netch80
                        02.11.2021 11:22

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

                        У меня 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 невозможна, и исходящие запросы откладываются до восстановления.
                        Если попытаться это описать в машине состояний, обнаружится, что сложность уже давно зашкалила за возможности укладки в голове не только среднего, но и ведущего программиста;\\ и решить можно только тщательно проработав схему в виде чистых состояний на двух уровнях, ну а саму эту схему желательно держать в верифицируемом виде (увы, пока не сподвиглись).


                      1. laatoo
                        03.11.2021 02:22

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


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


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


                      1. netch80
                        03.11.2021 13:38

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

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

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

                        > В SIP есть свой

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

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

                        Что должны обозначать эти коды?


                      1. laatoo
                        03.11.2021 13:46

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

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


                      1. netch80
                        03.11.2021 13:47

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

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


                      1. laatoo
                        03.11.2021 14:07

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

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


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


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


                      1. netch80
                        03.11.2021 14:17

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

                        Нет.

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

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


                      1. 0xd34df00d
                        02.11.2021 11:06
                        +1

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

                        Даже количество стейт-машин неограничено, не то что состояний каждой из них.


                        Тестируется не конкретная машина, а язык по их описанию.


                1. netch80
                  02.11.2021 10:30

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

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

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


                  1. 0xd34df00d
                    02.11.2021 11:14
                    +1

                    Анализатор этого описания проверяет известными средствами все пути.

                    Какими известными, если путей бесконечное число?


                    1. netch80
                      02.11.2021 11:37

                      > Какими известными, если путей бесконечное число?

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

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

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


                      1. 0xd34df00d
                        02.11.2021 22:20
                        +1

                        и других переходов в A, B, P нет.

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


                      1. BugM
                        03.11.2021 00:03

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


                      1. netch80
                        03.11.2021 13:39

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

                        То есть переходы есть? А как тогда вообще можно что-то доказать?


              1. laatoo
                02.11.2021 06:21
                -1

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

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


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

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


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

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


                1. netch80
                  02.11.2021 10:03

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

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

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

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

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

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


                  1. laatoo
                    02.11.2021 11:02
                    -1

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

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


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


                    1. netch80
                      02.11.2021 11:31

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

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

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

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

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

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

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

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


        1. sshikov
          31.10.2021 14:39
          +7

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

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

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


          1. DistortNeo
            31.10.2021 16:29
            +3

            Тесты — они ловят баги (хотя и не всегда)

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


            1. sshikov
              31.10.2021 16:47
              +1

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

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

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

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


          1. laatoo
            03.11.2021 12:25

            заказчик хочет завтра продукт на рынок выпустить, и текущее качество его устраивает. То есть, его цели на этом этапе — они как бы уже достигнуты. А вы ему говорите — не, погоди, мы еще на 100% тестами не покрыли

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


            1. sshikov
              03.11.2021 12:29

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


      1. DistortNeo
        31.10.2021 16:31
        +2

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

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


        1. sshikov
          31.10.2021 17:03

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


  1. realchel
    31.10.2021 16:37
    -4

    всем читать Чистый Agile и подобные вопросы не будут возникать


  1. kamaltatyana
    01.11.2021 09:36
    -4

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

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

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

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

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


    1. netch80
      01.11.2021 10:08
      +2

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

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

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

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

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

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

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


      1. kamaltatyana
        01.11.2021 12:04

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

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


        1. netch80
          01.11.2021 12:25

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

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

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

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

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


    1. nin-jin
      01.11.2021 10:13
      +7

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


  1. oxidmod
    01.11.2021 21:52
    +1

    У многих резкое сопротивление тестам, потому что есть опыт, когда тесты не помогли поймать багу и они типа ненадежны. Но ненадежны лишь те тесты, которые написаны абы как. Для себя вынес пару выводов:
    1. Тесты — это зафиксированные в коде спецификации. Если в разработку заходят задачи с детальными спеками, то писать тесты легко, по сути переводите спеки в код. Если же детальных спек нет, то тесты нужны вдвойне. В ходе накидывания тест кейсов вы еще до написания кода увидите где и что в условия задачи не сходится. Значит мы можем выделить и уточнить скользкие места еще даже не написав ни строчки кода. Бывало даже такое, что задача вообще снималась со спринта, потому что после детального анализа оказывалось, что ее требования противоречат друг другу. И лучше такое выявить раньше, чем под конец спринта.

    2. Когда первому пункту не уделяют достаточно внимания, то и тесты выходят дырявыми. Дырявые тесты не падают там, где должны и проверять на сколько твои тесты качественные можно и нужно. Для этого есть даже специальные подходы со своими инструментами. Я говорю о мутационном тестировании. В моем случае (разработка на PHP) — это прекрасная либа infection


  1. Ar2emis
    02.11.2021 14:08

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


  1. Sergei_Erjemin
    03.11.2021 15:21

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

    Честно попытался освоить 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)))


    1. Mikluho
      05.11.2021 13:47

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

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

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

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