Unit-тесты и PHPUnit

p

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

Клиенты часто жалуются, что одни и те же Unit-тесты иногда проходят, иногда нет. Это классический признак проблем с окружением: плавающие зависимости, разная версия PHP на локальной машине и сервере CI, нефиксированные версии PHPUnit. В 2026 году PHPUnit 11.x требует строго PHP 8.3+, а старые проекты на PHP 7.4 просто не смогут запустить новые тесты корректно. Стандартный выход — всегда фиксировать версию PHPUnit в composer.json и использовать файл .phpunit.dist.xml с явным указанием параметров окружения.

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

Вторая проблема — тесты тестируют не то, что нужно: путаница между Unit, Integration и Functional tests

Клиенты считают, что любой тест с PHPUnit — это Unit-тест. На самом деле PHPUnit — это фреймворк для написания разных типов тестов, но большинство разработчиков пишут интеграционные тесты (с реальной БД, с реальным HTTP-запросом), называя их «юнит-тестами». Разница критична: Unit-тест изолирует один класс, мокая все зависимости. Интеграционный тест проверяет связку нескольких классов. Смешение этих типов приводит к тому, что покрытие кода измеряется некорректно, а время прогона вырастает с 10 секунд до 10 минут.

Результат — unit-тесты работают за миллисекунды, а не за секунды. Покрытие кода становится реальным: вы видите, какие методы действительно изолированы, а какие «случайно» проходят из-за подключения всей инфраструктуры. Скорость прогона unit-тестов в 2026 году должна быть менее 1 секунды на 100 тестов — иначе это не unit, это интеграция.

Третья проблема — тесты ничего не проверяют: пустые assert и отсутствие data provider

Частая картина: метод testSomething() содержит один вызов assertTrue(true) или вообще без assert (тест проходит, потому что не выброшено исключение). Формально покрытие есть, а фактически — нет проверки логики. Ещё хуже, когда один и тот же тест прогоняется с одними и теми же данными, не покрывая граничные случаи. PHPUnit предоставляет инструмент Data Provider, позволяющий запускать один тест с десятками разных наборов входных данных, но разработчики его игнорируют.

После внедрения строгих правил по assert и обязательному использованию Data Provider вы получаете тесты, которые при падении сразу показывают точные входные и выходные данные. Устраняется ситуация «зелёный тест, а код сломан». Coverage report становится честным: строки кода действительно проверяются, а не просто выполняются.

Четвёртая проблема — тесты мешают рефакторингу: хрупкие тесты, привязанные к реализации

Когда вы меняете название приватного метода или переименовываете параметр, десятки тестов падают, хотя публичный интерфейс класса не изменился. Это признак тестов, которые тестируют детали реализации, а не поведение. PHPUnit поддерживает @covers аннотацию, которая явно декларирует, какой класс/метод тестируется. Разработчики её не используют, потому и тесты «ломаются» при любом рефакторинге. В 2026 году хороший тон — тестировать исключительно публичные методы через @covers ClassName, а все приватные методы тестировать только косвенно, через вызов публичных.

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

Пятая проблема — тесты не интегрированы в CI/CD pipeline: ручной запуск и игнорирование метрик

Многие разработчики запускают тесты локально раз в день, а в CI тесты либо не настроены, либо настроены на прогон всех тестов (включая медленные интеграционные) при каждом коммите, что занимает 20-30 минут. В 2026 году стандарт — многоступенчатый pipeline: сначала unit-тесты (группа unit, выполняются за 5 секунд), затем статический анализ, затем интеграционные тесты. Если unit-тесты падают, pipeline останавливается мгновенно, не тратя ресурсы на остальное.

Результат: тесты становятся частью культуры непрерывной поставки (CD). Каждый коммит проверяется автоматически, пороги покрытия защищают от деградации, а метрики дают прозрачность всей команде. Время детекции дефектов сокращается с часов до секунд.

Шестая проблема — игнорирование рискованных тестов и нестабильных тестов (flaky tests)

Когда тесты падают по неочевидным причинам (например, из-за таймаута сети, порядка выполнения тестов, состояния shared memory), разработчики часто просто перезапускают CI, не разбираясь. Со временем накапливается 5-10% «серых» тестов, которые периодически падают. PHPUnit поддерживает атрибут @runInSeparateProcess и @preserveGlobalState для изоляции, но их редко используют. В 2026 году единственный способ борьбы с flaky-тестами — это жёсткая изоляция каждого unit-теста и запрет на использование static-свойств без сброса.

После внедрения изоляции и детекции flaky-тестов вы получаете pipeline, который либо зелёный, либо содержит реальную ошибку. Никаких «попробуйте перезапустить сборку». Стабильность тестов становится выше 99,9%, и команда доверяет результатам CI полностью.

Добавлено: 23.04.2026