company_banner

Общая картина модульного тестирования

https://gundars.me/php/unit-testing-php-big-picture/
  • Перевод


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

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

Что такое тестирование?


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

Тестирование ПО определяется как «расследование, проведённое с целью предоставления заинтересованным сторонам информации о качестве продукта». Этому противопоставляется «тестирование ПО — это пустая трата бюджета проекта разработчиками, которые не делают ничего важного, а затем просят ещё времени и денег, потому что «ничего» может быть весьма дорогим». Тут ничего нового.

Вот моя краткая история становления тестирования:

  • 1822 — Разностная машина (Difference engine) (Чарльз Бэббидж).
  • 1843 — Аналитическая машина (Analytical engine) (Ада Лавлейс).
  • 1878 — Эдисон вводит термин «баг».
  • 1957 — Тестирование и отладка программ (Чарльз Бэйкер).
  • 1958 — Первая команда тестирования ПО (Джеральд Вайнберг).
  • 1968 — Кризис ПО (Фридрих Бауэр).
  • 1970-е — Модель «водопад», реляционная модель, декомпозиция, критический анализ (Walkthrough), проектирование и инспектирование кода, качество и метрики, шаблоны проектирования.
  • 1980-е — CRUD-анализ, архитектура системы, автотестирование, V-модель, надёжность, стоимость качества, способы использования, шаблоны ООП-проектирования.
  • 1990-е — Scrum, usability-тестирование, MoSCoW, эвристическое тестирование, автоматизация ПО и тестирования.

Если вы относитесь к поколению миллениалов, как я, то, возможно, будете поражены, что команды тестировщиков существовали ЗАДОЛГО до вашего рождения. Остановитесь на минутку, вдохните, выдохните, успокойтесь.
История показывает, как в течение времени менялся тип тестирования, которое считалось «достаточно хорошим» для заинтересованных сторон. Примерные фазы, на что ориентировались при тестировании:

  • … — 1956 отладка
  • 1957 — 1978 демонстрация
  • 1979 — 1982 разрушение (destruction)
  • 1983 — 1987 оценка
  • 1988 — … предотвращение

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

Чем на самом деле является тестирование?


Есть разные классификации тестирования ПО. Чтобы лучше понимать место модульного тестирования, упомяну лишь о наиболее широкораспространённых подходах.

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

Статическое и динамическое тестирование


Статическое тестирование проводится без исполнения кода. Сюда относится корректура, проверка, ревизия кода (при наблюдении за работой другого / парном программировании), критический анализ, инспекции и так далее.

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

«Ящичный» подход


Согласно этому подходу, все тесты ПО делятся на три вида ящиков:

  • Тестирование типа «белый ящик» проверяет внутренние структуры и модули, игнорирует ожидаемую функциональность для конечных пользователей. Это может быть тестирование API, внесение неисправностей (fault injection), модульное тестирование, интеграционное тестирование.
  • Тестирование типа «чёрный ящик» больше интересуется тем, что делает ПО, а не как делает. Это означает, что тестировщики не обязаны ни разбираться в объекте тестирования, ни понимать, как он работает под капотом. Такой тип тестирования нацелен на конечных пользователей, их опыт взаимодействия с видимым интерфейсом. К «чёрным ящикам» относится тестирование на основе моделей, тестирование способов использования, таблицы переходов состояний, спецификационное тестирование и т. д.
  • Тестирование типа «серый ящик» проектируется со знанием программных алгоритмов и структур данных (белый ящик), но выполняется на пользовательском уровне (чёрный ящик). Сюда относится регрессионное тестирование и шаблонное тестирование (pattern testing).

Теперь, чтобы запутать вас, скажу, что модульное тестирование может относиться и к «чёрному ящику», поскольку вы можете разбираться в тестируемом модуле, но не во всей системе. Хотя для меня оно по-прежнему «белый ящик», и предлагаю вам с этим согласиться.

Уровни тестирования


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

  1. Модульное тестирование.
  2. Интеграционное тестирование.
  3. Тестирование интерфейсов компонентов.
  4. Системное тестирование.
  5. Эксплуатационное приёмочное тестирование.

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

Типы тестирования


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

  • Регрессионное тестирование.
  • Приёмочное тестирование.
  • Дымовое (smoke) тестирование.
  • UAT
  • Разрушительное (Destructive) тестирование.
  • Тестирование производительности.
  • Непрерывное тестирование.
  • Usability-тестирование.
  • Тестирование безопасности.

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

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

Что такое модульное тестирование?


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



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

И это справедливо. У модульных тестов масса достоинств. Они:

  • Изолируют каждую часть программы и проверяют её корректность.
  • Помогают рано обнаруживать проблемы.
  • Заставляют разработчиков мыслить в рамках входных, выходных и ошибочных условий.
  • Придают коду удобный для тестирования вид, облегчают будущий рефакторинг.
  • Упрощают интегрирование рабочих (!) модулей.
  • Частично заменяют техническую документацию.
  • Заставляют отделять интерфейс от реализации.
  • Доказывают, что код модуля работает так, как ожидалось (хотя бы математически).
  • Могут использоваться как низкоуровневые наборы регрессионных тестов.
  • Демонстрируют прогресс в незавершённой системной интеграции.
  • Снижают стоимость исправления багов (с TDD — ещё больше).
  • Позволяют улучшать архитектуру приложения с помощью определения ответственности модулей.
  • Если вы можете это протестировать, то можете присоединить к своей системе.
  • Модульное тестирование — это ВЕСЕЛО!

Однако, есть определённые ограничения, о которых вы подумали, вероятно, при чтении этого списка:

  • Модульное тестирование не вылавливает ошибки интегрирования.
  • Каждое булево выражение требует как минимум двух тестов, и количество быстро растёт.
  • Модульные тесты столь же глючные, как и тестируемый ими код.
  • Привязка тестов к паре конкретных фреймворков или библиотек может ограничить рабочий процесс.
  • Большинство тестов пишется после завершения разработки. Печально. Используйте TDD!
  • Возможно, после маленького рефакторинга система будет работать как прежде, но тесты будут сбоить.
  • Вырастает стоимость разработки.
  • Человеческая ошибка: комментирование сломанных тестов.
  • Человеческая ошибка: добавление в код обходных путей специально для прохождения модульных тестов.

Последнее убивает меня больше всего. (Почти) в каждом проекте прямо в исходном коде рабочего приложения я нахожу строки наподобие «если это модульный тест, грузить суррогатную SQLite базу данных, в противном случае грузить другую БД», или «если это модульный тест, не отправлять письмо, в противном случае отправлять», и так далее. Если у вашего приложения плохая архитектура, не притворяйтесь, что можете исправить паршивое ПО с помощью хорошего прохождения тестов, оно от этого не станет лучше.

Я часто обсуждал с коллегами и клиентами, что такое хороший модульный тест. Он:

  • Быстрый.
  • Автоматизированный.
  • Полностью управляет всеми своими зависимостями.
  • Надёжен: может запускаться в любом порядке, вне зависимости от других тестов.
  • Может запускаться только в памяти (никаких взаимодействий с БД, чтений/записей в файловой системе).
  • Всегда возвращает один результат.
  • Удобен для чтения и сопровождения.
  • Не тестирует SUT-конфигурацию (system under test).
  • Имеет чётко определённую ЕДИНСТВЕННУЮ ЗАДАЧУ.
  • Хорошо именован (и достаточно понятно, чтобы избежать отладки только ради выяснения, что же сбоит).

Тем, кто ухмыльнулся, прочитав «автоматизированный»: я не имел в виду интегрирование PHPUnit или JUnit в CI-конвейеры. Речь идёт о том, что если вы меняете код, сохраняете его и не знаете, проходят ли модули свои тесты, то они не автоматизированы, а должны бы. Выигрышный вариант — отслеживание файлов (File watcher).

Что нужно подвергать модульному тестированию?


В нормальных системах модульные тесты нужно писать для:

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

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

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

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

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

Что НЕ нужно тестировать


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

  • Функциональность за пределами контекста (scope) модулей (!)
  • Интеграция модулей с другими модулями (!)
  • Неизолированное поведение (неимитируемые (unmockable) зависимости, настоящие БД, сеть)
  • Приватные, защищённые методы.
  • Статичные методы.
  • Внешние библиотеки.
  • Ваш фреймворк.

Уверен, не следует применять модульное тестирование ни к чему из вышеперечисленного, кроме статичных методов. Мне нравится аргументировать, что статичность, по сути, означает процедуральность, причём в многих случаях процедуральность глобальную. Если статичный метод вызывает другой статичный метод, то эту зависимость нельзя переопределить. А это значит, что вы теперь тестируете не изолированно. И тогда это уже не модульное тестирование. С другой стороны, это же часть кода, которая может жить сама по себе, у неё есть предназначение, и её нужно тестировать, чтобы удостовериться: какую бы часть этой бестолковой системы ни вызвала тестируемая часть кода, та не сломается. Поэтому считаю, что тестировать статичные методы можно, если вы уверены, что выходные данные вашего теста не сможет изменить никакой другой тест, и что язык или фреймворк позволят тестировать нативно.

Как писать модульные тесты?


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

Если «затем тестируйте его» недостаточно, то на laracasts.com есть очень хорошие видео про модульное тестирование PHP. Есть и масса сайтов, посвящённых той же задаче в других языках. Не вижу смысла объяснять, как я выполняю модульное тестирование, потому что инструменты меняются довольно быстро, и когда вы прочитаете этот текст, я могу переключиться с PHPUnit на Kahlan. Или нет. Кто знает.

Но ответить на первый вопрос (как писать код, пригодный для модульного тестирования) гораздо легче, и вряд ли ситуация сильно изменится со временем:

  • SOLID
  • DRY
  • Отсутствие новых ключевых слов в конструкторе.
  • Отсутствие циклов в конструкторе (и переходов, если это оговаривается).
  • Отсутствие статичных методов, параметров, классов.
  • Отсутствие методов setup(): объекты должны быть полностью инициализированы после конструирования.
  • Отсутствие синглтонов (глобального состояния) и прочих нетестируемых антипаттернов.
  • Отсутствие всемогущих объектов (God objects).
  • Отсутствие классов со смешанной функциональностью (mixed concern classes).
  • Отсутствие скрытых зависимостей.

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

В заключение: модульные тесты очень важны как для разработчиков, так и для бизнеса. Их нужно писать, существуют отработанные методики, которые помогут вам легко покрыть модули тестами, в основном с помощью подготовки самих модулей. Но все эти методики не имеют смысла без знания теории тестирования, описанной в этой статье. Нужно уметь отличать модульные тесты от тестов других типов. И когда у вас в голове будет ясное понимание, то и писать тесты вам станет гораздо легче.
Mail.Ru Group 655,92
Строим Интернет
Поделиться публикацией
Похожие публикации
Комментарии 39
    0

    Шёл 2018 год. В статьях о тестировании мы всё ещё читаем о том, почему нам нужно писать тесты.

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

            Вам повезло. Я вот был в проекте, где написание модульных тестов было запрещено. Прямым указом. Место работы я в итоге сменил...

              0
              Как говорил Иисус Христос:
              Не давайте святыни псам и не бросайте жемчуга вашего перед свиньями, чтобы они не попрали его ногами своими и, обратившись, не растерзали вас.
          0
          Если бы их большинство писало… Лично я узнал как правильно TDDшить в embedded только в этом году.
            0
            Было бы интересно прочитать:)
              0
              Лучше, чем в «Test-Driven Development for Embedded C», J.W.Grenning, я не напишу.
          +3

          В оргинале «Отсутствие новых ключевых слов в конструкторе» звучало как «No new keyword in constructor». Я не специалист в PHP, на мне кажется, что тут имелось в виду, что не стоит создавать какие-либо объекты в конструкторе класса через new.

            +1

            Про "статичные методы" уже был комментарий? Если нет, то вот.

              +3
              Типичные ошибки:
              1. Смешивают в кучу элементы разных ортогональных классификаций: по тестируемому объекта, по проверяемым характеристикам, по процессу тестирования.
              2. Правильно замечают, что модульное тестирование проверяет конкретный код без зависимостей, и тут же предлагают не тестировать приватные методы, мол они протестируются косвенно.
              3. Подменяют понятие «хорошей архитектуры» понятием «архитектура ориентированная на модульные тесты». Для второй характерно выпячивание всех кишок в публичный интерфейс с соответствующими неудобствами в использовании.
              4. «Модульное тестирование — это ВЕСЕЛО!» — без конца мокать всё подряд — это совсем не весело.
              5. Приводятся какие-тот странные ограничения на прикладной код вызванные тестами. Хотя реализация должна определяться требованиями. И это тесты должны подстраиваться под реализацию и уметь проверять любую, в том числе синглтоны, божественные объекты и пр.

              Подробнее я расписал всё тут: habr.com/post/351430
                0
                А что не так с третьим пунктом, конкретно с нежеланием тестировать приватные методы? Ваши тесты не должны меняться, если вы приватную функцию отрефакторите в две (и соответствующим образом модифицируете вызовы), в этом как бы суть сокрытия этой логи, на то она и приватная
                  0
                  То, что модульные тесты — это тесты белого ящика, а тестирование публичного интерфейса — тестирование чёрного. Как вы протестируете, что, например, реализовали qsort правильно (и он не выродился в bubble из-за ошибки вычисления медианы) без доступа к приватной функции, реализующей одну итерацию?
                    0
                    Как вы протестируете, что, например, реализовали qsort правильно


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

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

                      Тут важный момент — как проявляется знания тестов о тестируемом коде. Описанный вами случай, когда нам прям нужно знать что мы реализовали qsort, как по мне больше экзотика. В 99% случаев нас не должны интересовать такие детали.


                      Белый ящик в моем понимании — это дублирование реализации в тестах. Это высокая связанность тестов и тестируемого кода. Это очень хрупкие и в целом бесполезные тесты.


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


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

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

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


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

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


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

                          0
                          В результате контракт «сортировщика» внезапно начинает требовать на вход не только массив, но и «логгер»


                          Не очень понимаю, откуда, например, в такой иерархии

                          interface Sorter { public function sort(array $array): array }
                          
                          class MegaSorter implements Sorter 
                          {
                             public function __construct(LoggerInterface $logger) {...} 
                          
                             public function sort(array $array): array {...}
                          }
                          


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

                          Ну вот представьте, у вас есть один публичный метод и 10 приватных. Через «публичный» интерфейс тестировать — комбинаторный взрыв.


                          Если у вас до 10 приватных методов можно добраться разными комбинациями через комбинаторный взрыв, то это немножечко пахнет нарушением SRP

                          Через REST — одно, прикладному коду — другое, сериализатору — третее, менеджеру памяти — четвёртое, тестам — пятое, соседним классам в пакете — шестое, соседним пакетам в скоупе — седьмое


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

                            Очевидно, вот тут:


                            public function __construct(LoggerInterface $logger) {...} 

                            Но судя по вопросу вы используете IoC контейнер, который создаёт вам объекты самостоятельно, скрывая эту часть интерфейса. Спрашивается, нафига выпячивали, чтобы потом скрывать? Смотрите, как то же самое делается без выворачивания кишок наружу:


                            interface Sorter { public function sort(array $array): array }
                            
                            class MegaSorter implements Sorter 
                            {
                               public function __construct() {
                                   $this->logger = AmbientContext.get(LoggerToken);
                               } 
                            
                               public function sort(array $array): array {...}
                            }

                            Если у вас до 10 приватных методов можно добраться разными комбинациями через комбинаторный взрыв, то это немножечко пахнет нарушением SRP

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


                            Но вообще опять попахивает нарушением SRP.

                            99% разработчиков, молящихся на SRP вообще не представляют что такое Responsibility. Нет, SRP — это не про "каждому действию по отдельному классу", а про "не стирать цветное бельё с белым".


                            На примере того же сортировщика можете продемонстрировать, когда такое нужно?

                            Что именно? Разные интерфейсы разным потребителям? Я бы лучше послушал про случаи, когда всем потребителям нужен один и тот же интерфейс.)

                              0
                              Очевидно, вот тут:


                              Неочевидно. Конструктор не является частью контракта Sorter. Я могу использовать другой сортер с другим конструктором. Могу без него. Контракт стабилен.


                              public function __construct() {
                              $this->logger = AmbientContext.get(LoggerToken);
                              }


                              Вот за такие вещи я и не люблю фреймворки типа Yii. Это же откровенная лапша в коде. Если в контексте не будет этого токена, то весь код, особенно клиентские библиотеки, которые еще не обновились и почему то тоже его хотят — просто развалится. IoC с DI контейнером как раз таки позволяют вам вертеть зависимостями как вам угодно.

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


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

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

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


                              Либо вы предлагаете мне показать вам GodObject, либо я не понял вашего вопроса.
                                0
                                Конструктор не является частью контракта Sorter.

                                Он является частью контракта MegaSorter.


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

                                Не развалится, ибо будет использована реализация по умолчанию.


                                IoC с DI контейнером как раз таки позволяют вам вертеть зависимостями как вам угодно.

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


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

                                Давайте без демагогии.


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

                                Какой именно параметр улучшится?

                            0
                            > начинает требовать на вход не только массив, но и «логгер», «квантайзер», «менеджер памяти», «хеш-функцию» и прочую ерунду

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

                            > это прям требование, а не деталь реализации.

                            а можете привести более… приближенный к реальности пример?

                            > Разным потребителям должны быть доступны разные уровни интерфейсов.

                            Вы сейчас как-то все так лихо перемешали что я даже затрудняюсь что-либо прокомментировать тут… Причем тут модульные тесты и REST?

                            > Ну вот представьте, у вас есть один публичный метод и 10 приватных.

                            Да, согласен, но в реальной жизни такие ситуации крайне редки (обычно 10 приватных методов -> что-то с разделением ответственности пошло не так). В целом я отказываюсь обсуждать этот вопрос на сферических конях в вакууме ибо все просто скатится к пустой демагогии.
                              0
                              а можете привести более… приближенный к реальности пример?

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


                              Причем тут модульные тесты и REST?

                              А при чём тут тесты? Тут мы обсуждали приватный и публичный интерфейсы. Тесты — один из пользователей кода, которому нужен соответствующий уровень доступа.

                                0
                                Нам нужно проверить, что алгоритм реализован правильно

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


                                А так комбинация из покрытия типами + примеры — этого достаточно что бы проверить требуемое поведение.


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

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


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


                                Тут мы обсуждали приватный и публичный интерфейсы.

                                Скорее то, сколько знаний о реализации мы экспоузим в тэстах. И я не очень понимаю причем тут http api и модульные тесты. Для приемочных/интеграционных тестов да, можно говорить. Модульные действуют на другом уровне.


                                Да и те же http api можно частично покрыть типами что бы уменьшить необходимость в дорогих тестах.

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

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


                                    что в нём сложно тестировать приватные методы.

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

                                      0

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

                                        0
                                        > Я сторонник энкапсуляции, но не сокрытия.

                                        можете раскрыть столь глубокую мысль? То есть information hiding это бушит? Или вы про то что кастыль вида private/public это булшит? Если последнее — то я вас поддержу.

                                        > но если там какая-то сложная логика, то протестирую сначала эту функцию

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

                                            0
                                            Ну смотря как трактовать information hiding.

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

                                            Я все верно уловил?
                                              +1
                                              Ну типа того, да.
                      0
                      2. не просто «без зависимостей» а «в изоляции». Иногда (в случае сложных алгоритмов) это подразумевает тестирование приватных методов, но это крайне редко. Опять же основная рекомендация для таких тестов — нещадно их удалять когда они начинают мешать.

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

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

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

                      К вашей статье добавлю ссылку где в целом все эти тезисы разбираются: [J B Rainsberger — Integrated Tests Are A Scam](https://www.youtube.com/watch?v=VDfX44fZoMc)
                        0
                        не просто «без зависимостей» а «в изоляции».

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


                        Опять же основная рекомендация для таких тестов — нещадно их удалять когда они начинают мешать.

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


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

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

                        Интеграционные тесты — это смазывать фокус внимания.

                        Почитайте, пожалуйста, мою статью не по диагонали. Очень вас прошу, я же так старался. Интеграционные тесты не противопоставляются модульным, а дополняют их. А вот компонентные тесты противопоставляются тандему модульных и интеграционных.


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

                        У меня сейчас перед глазами два проекта:


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


                        2. Применяется компонентное тестирование. Мока всего два: http и location. Регистрируются они один раз для всех тестов. Сами тесты весьма просты и не требуют сложной подготовки. При этом покрытие кейсов как у модульных с интеграционными вместе взятыми. Что не мешает точно понимать в каком модуле проблема, ибо компонентное тестирование идёт каскадом.

                        К вашей статье добавлю ссылку где в целом все эти тезисы разбираются

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

                          0
                          Давайте без терминологических споров.

                          ну так вы же их разводите.


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

                          Декомпозиция не имеет практической пользы?


                          У меня сейчас перед глазами два проекта:

                          увы по вашему описанию я могу сделать два вывода:


                          • слишком много зависимостей?
                          • так себе ситуация в JS с mock фреймворками?

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


                          как следует разобраться в теме.

                          может быть вы как следует не разобрались в том о чем он вещает и только лозунги слушали? Да, я согласен что сам по себе доклад в целом не отвечает на вопрос "как делать" и для этого лично мне пришлось немало времени потратить что бы понять "как так он живет хорошо с модульными тестами". Ну то есть вопрос о e2e тестах скажем у него за кадром остался, вопрос о event driven подходах и снижении количества зависимостей до минимума так же. Но все же...

                            0
                            удобство изоляции отдельного модуля — не имеет практической ценности.

                            Декомпозиция не имеет практической пользы?

                            Изоляция и декомпозиция — ортогональные понятия. Впрочем, декомпозиция, ради декомпозиции, тоже не имеет практической ценности.


                            слишком много зависимостей?

                            Да вроде не слишком.


                            так себе ситуация в JS с mock фреймворками?

                            Какая разница через что мокать, если в одном проекте это надо делать перед каждым тестом, а в другом — вообще не надо?


                            У меня же перед глазами другая проблема. Прекондишен для функционала на уровне базы

                            Я не очень понял суть проблемы. Ну, поднять in-memory базу не такая большая сложность. Зато точно будете уверены, что ваш запрос базой будет интерпретирован верно.


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

                            Предлагаю потратить ещё немного времени, чтобы понять "как так он живёт хорошо с компонентными тестами" :-)

                              0
                              Изоляция и декомпозиция — ортогональные понятия.

                              Не согласен. У этих понятий есть пересечение. И это важно.


                              Впрочем, декомпозиция, ради декомпозиции, тоже не имеет практической ценности.

                              согласен.


                              Ну, поднять in-memory базу не такая большая сложность.

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


                              что ваш запрос базой будет интерпретирован верно.

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


                              И я буду намного больше уверен что все работает.


                              "как так он живёт хорошо с компонентными тестами"

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


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


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

                                0
                                Не согласен. У этих понятий есть пересечение. И это важно.

                                Какое пересечение есть у нарезки хлеба и упаковки его в пакеты?


                                проблема с подготовкой различных прекондишенов для различных сценариев (в том числе и негативных).

                                А в чём там проблема?


                                // Создали статью от тест-админа
                                const article = habr.createArticle({ title: 'Hello' })
                                
                                // Зарегались под новым пользователем с рандомным уникальным именем
                                const person = habr.signUp({ name : stub.person.name , password: 'bar' })
                                
                                // попытались удалить
                                shouldFail( ()=> article.remove() , new Error( 'You do not have rights to delete "Hello" article' ) )

                                По объему кода разницы с модульными тестами не особо много.

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


                                По степени хрупкости — так же.

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


                                По скорости работы — разница огромная.

                                Зависит от инструментария. Для примера — TestBed в Ангуляре, инициализация которого занимает 0.2с на топовой рабочей станции. Так как в модульных тестах нужно без конца подсовывать свои моки, то TestBed ресетится перед каждым тестом. Получается 30 тестов занимают минимум 5 секунд. С компонентными тестами, его достаточно один раз настроить перед тестами и далее не трогать, что позволяет вырубить авторесет и ускорить тесты в 10 раз. Вот тебе и "быстрые" модульные тесты.


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

                                Покраснел модульный тест, проблема может быть:


                                • в тесте
                                • в модуле
                                • в моке

                                Покраснел компонентный тест, проблема может быть:


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

                                я тестирую запрос или то как база интерпритирует запросы?

                                Зависит от того хотите ли вы чтобы у вас правильно запросы генерились или же чтобы приложение работало. Вы можете не знать все тонкости синтаксиса, в СУБД может быть бага, недокументированное ограничение. Виноваты-то может быть и разработчики СУБД, но проблема-то ваша. И чем раньше вы узнаете о ней, тем лучше.


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

                                А сколько надо, чтобы выглядеть авторитетом?


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

                                SPA в основном.


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

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


                                Как никак стандартную библиотеку я ж не подменяю

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

                      0

                      В книге "чистая архитектура" предлагается тестировать api библиотек, которые вы используете, чтобы знать, что новое обновление не изменило её интерфейс и чтобы лучше понимать её работу.
                      Что вы думаете об этом?

                        0

                        На моё сознание аргументы автора легли с немного иными приоритетами, а именно: чтобы быть уверенным, что твоё понимание "как оно работает и что делает" соответствует реальности и чтобы иметь конспект заведомо достоверных примеров.


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

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

                      Самое читаемое