Инструменты тестирования PHP для безопасного рефакторинга старых кодовых баз
Инструменты тестирования PHP помогают снизить риск в legacy-приложениях с помощью unit-, fixture- и browser-тестов до того, как вы переименуете классы, разделите файлы или удалите лишний код.

Почему старый PHP-код кажется рискованным
Старые PHP-приложения редко ломаются в одном очевидном месте. Они ломаются тихо и раздражающе. Небольшое изменение в одном файле может поменять значение сессии, пропустить письмо или повлиять на cron-задачу, которая сработает только через несколько часов.
Во многом этот страх связан со скрытыми побочными эффектами. В старом коде часто используются глобальное состояние, общие хелперы, include-файлы и функции, которые делают куда больше, чем подсказывает их название. Метод saveOrder() может одновременно отправлять квитанцию, обновлять остатки, писать строку в лог и ставить flash-сообщение для следующей загрузки страницы.
Из-за этого даже простые изменения кажутся крупнее, чем они есть. Вы обновляете правило расчёта налога в форме оформления заказа, и вдруг экспорт для бэк-офиса выглядит неправильно. Вы приводите в порядок helper, который форматирует даты, и плановый отчёт падает, потому что другая часть приложения ждала старый, сломанный формат.
Во многих старых кодовых базах всё ещё и смешано в одном месте. HTML, SQL, обработка запросов, валидация и бизнес-логика живут в одном файле. Когда код годами растёт именно так, люди перестают доверять собственным правкам. Они больше читают, меньше меняют и всё равно нервничают перед нажатием кнопки deploy.
Отсутствие тестов только усиливает это напряжение. Без страховочной сетки разработчики начинают гадать: пройтись по нескольким страницам, отправить одну форму, проверить почту и надеяться, что ничего ещё не сломалось. Это не лень. Так работают команды, когда приложение не даёт быстрого способа доказать, что ничего не испорчено.
Самое неприятное — это тихие поломки. Фатальная ошибка быстро заметна. А вот пропавшее письмо, неверная скидка или cron-задача, которая пропускает редкий случай, могут оставаться незамеченными днями. К тому моменту изменение, которое их вызвало, уже затеряется под несколькими следующими deploy.
Поэтому команды откладывают даже скучные исправления. Переименовать функцию, удалить дублирующийся код или разделить огромный controller должно быть обычной работой. В старом PHP-приложении это может ощущаться как попытка вытащить один провод из стены, где все кабели без подписей.
Без инструментов тестирования PHP уборка по умолчанию кажется рискованной. С ними страх уменьшается, потому что код начинает отвечать.
Сначала постройте страховочную сетку, потом убирайте код
Начните с того, чтобы отметить пользовательские сценарии, которые сильнее всего ударят, если сломаются. В старом PHP-приложении это обычно пути, связанные с регистрацией, платежами, созданием заказов, счетами и действиями сотрудников, которые меняют данные клиентов.
Большинство инструментов тестирования PHP помогают только после того, как вы решили, что именно защищать. Не нужно сначала покрывать весь код. Нужен небольшой набор проверок вокруг частей, с которыми пользователи работают каждый день, и тех, что приносят деньги.
Короткий список приоритетов помогает не распыляться:
- оформление заказа или отправка заказа
- вход и сброс пароля
- обновления счёта, возврата или платежа
- административные правки, которыми сотрудники пользуются ежедневно
- один отчёт или экспорт, от которого зависит бизнес
После этого пишите characterization tests. Эти тесты фиксируют то, как код ведёт себя сейчас, даже если поведение выглядит странно. Если старый метод обрезает ввод необычным способом, пропускает поле или возвращает неверный статус в редком случае, зафиксируйте это в тесте до любых изменений.
Это звучит наоборот, но работает. Вы не одобряете плохой код. Вы замораживаете текущее поведение, чтобы рефакторить с меньшим количеством догадок.
Покройте один известный баг до того, как тронете его код. Возьмите проблему, на которую жалуются люди, воспроизведите её тестом и добейтесь, чтобы он падал по правильной причине. Потом начинайте рефакторинг. Когда тест станет зелёным, вы будете знать, что исправление пришло именно из ваших изменений, а не случайно.
Сохраняйте примеры входных данных и ожидаемых результатов из случаев, похожих на production. Несколько реальных примеров полезнее двадцати выдуманных. Их можно обезличить и хранить как fixtures: заказ со скидочным кодом, повторная попытка неудачного платежа, неполный адрес или форма, отправленная дважды.
Такие примеры решают две задачи. Они ускоряют настройку тестов и убирают споры о том, как приложение должно работать. Когда старая PHP-кодовая база сопротивляется каждому изменению, такая ясность экономит часы.
Фреймворки для тестов, которые подходят для legacy PHP
Старым PHP-приложениям редко нужен сразу сложный стек тестирования. Им нужен инструмент, который нормально ставится, работает на текущей версии PHP и показывает понятные ошибки, когда что-то ломается.
PHPUnit по-прежнему остаётся безопасным вариантом по умолчанию для большинства задач по рефакторингу legacy PHP. Он работает с обычным PHP, старыми фреймворками и смешанными кодовыми базами, где половина приложения выглядит самописной, а другая половина — заброшенной. У него также самая широкая поддержка, поэтому IDE, CI-задачи, coverage-инструменты и примеры от других команд обычно работают без лишней драмы.
Pest работает поверх PHPUnit, поэтому часто ощущается как удобный шаг, а не как полная замена. Под капотом всё тот же движок, но сами тесты читаются чище и обычно занимают меньше строк. Для старых проектов это важно: людям проще продолжать писать тесты, если синтаксис не похож на бюрократию.
Если ваша команда уже знает PHPUnit, я бы не спешил его менять. Добавляйте Pest только в том случае, если более чистый стиль действительно поможет людям писать больше тестов, а не просто потому, что он выглядит современнее.
Codeception подходит для другого типа хаоса. Он позволяет одним набором настроек покрывать unit-тесты, API-проверки и acceptance-тесты, что полезно, когда в старом приложении бизнес-логика размазана по controller, формам и запросам к базе данных. Вместо того чтобы собирать несколько маленьких инструментов, можно держать большую часть работы в одном месте.
Простой способ выбрать:
- Используйте PHPUnit, если приложение старое, смешанное или его сложно предсказать.
- Используйте Pest, если хотите PHPUnit с более чистым синтаксисом.
- Используйте Codeception, если вам нужны unit-, API- и browser-style проверки в одном проекте.
Лучший выбор обычно тот, который команда может запускать и отлаживать уже сегодня. Если тест падает, кто-то должен понимать, где искать проблему, как прогнать его ещё раз и почему результату можно доверять. Знакомый инструмент, который ловит реальные поломки, лучше красивой настройки, к которой никто не хочет прикасаться.
Для большинства команд это означает начать с PHPUnit, добавить Pest, если писать тесты кажется слишком тяжело, и использовать Codeception, когда приложение проходит слишком много слоёв, чтобы изолированные unit-тесты могли показать всю картину.
Fixture-хелперы, которые сокращают время на подготовку
Ручная сборка тестовых данных быстро съедает время. В старых PHP-приложениях это обычно заканчивается тем, что люди пропускают тесты и рефакторят вслепую.
Среди инструментов тестирования PHP fixture-хелперы дают самое быстрое облегчение. Они создают правдоподобные записи с меньшим количеством шума, так что каждый тест может сосредоточиться на одном правиле, а не на десяти шагах подготовки.
Faker — самый простой старт. Он за секунды заполняет имена, даты, email, цены и другие распространённые поля. Реалистичные значения важнее, чем кажется. Legacy-форма может проходить с везде одинаковым "test", а потом ломаться, когда в код попадает настоящая фамилия, часовой пояс или формат даты.
Когда проекту нужны большие наборы примеров данных, Nelmio Alice делает их удобными и читаемыми в файлах. Это особенно полезно в запутанном магазине, CRM или бэк-офисе, где один экран зависит сразу от клиентов, заказов, платежей и прав доступа. Можно хранить несколько именованных сценариев в одном месте вместо того, чтобы собирать их заново в каждом тесте.
Symfony-проекты часто выигрывают от Zenstruck Foundry. Он делает создание объектов коротким и понятным, особенно когда у сущностей много связей. Тест может создать заказ с двумя позициями и просроченным купоном без громоздкой ручной сборки.
У команд на Laravel есть model factories, которые уже встроены в привычный способ работы многих приложений. Для проектов, где активно используется Eloquent, factories обычно дают самый быстрый путь к повторяемым записям. States здесь очень помогают. Оплаченные, черновые или отменённые записи проще доверять, когда каждое состояние создаётся одним определением factory.
Держите fixtures маленькими
Частая ошибка — собирать огромные наборы fixtures просто на всякий случай. Это замедляет тесты и скрывает причину падения.
Лучше работает более аккуратный подход:
- Начните с одной валидной записи по умолчанию.
- Переопределяйте только те поля, которые важны для теста.
- Создавайте только те связанные записи, которые код действительно читает.
- Переиспользуйте именованные сценарии, если нескольким тестам нужна одна и та же подготовка.
Если тест проверяет округление налога, ему, скорее всего, не нужна полная история клиента, маркетинговые предпочтения и пять старых счетов. Небольшие fixtures делают ошибки понятнее и облегчают последующие изменения.
Браузерные тесты для хрупких пользовательских сценариев
Некоторые баги не видны в unit-тестах. Экран входа может сломаться из-за пропавшего JavaScript-события. Форма checkout может отправить неправильные имена полей. Dashboard может загружаться, но одна сломанная кнопка способна заблокировать всю задачу.
Здесь помогают browser tests. Они открывают приложение так же, как это делает пользователь, кликают по реальным страницам и ловят поломки, которые не замечают тесты более низкого уровня. Они медленнее большинства инструментов тестирования PHP, поэтому лучше всего работают на небольшом наборе сценариев, на которые люди полагаются каждый день.
Laravel Dusk хорошо подходит для проектов на Laravel. Его удобно использовать для проверок входа, многошаговых форм, сброса пароля и базовых действий в dashboard. Если старое Laravel-приложение заставляет вас нервничать каждый раз, когда вы трогаете экраны авторизации или биллинга, Dusk может быстро вернуть спокойствие.
Symfony Panther хорошо подходит для Symfony-приложений, но работает и с обычными PHP-проектами. Он управляет настоящим браузером, поэтому помогает ловить проблемы с отображением, JavaScript, редиректами и поведением форм. Это полезно, когда кодовая база смешивает старые шаблоны, controller и немного front-end-кода, к которому никто не хочет прикасаться.
Codeception с WebDriver часто оказывается самым практичным выбором для старых server-rendered приложений. Он позволяет делать широкие end-to-end проверки, не заставляя вас полностью перестраивать тестовую среду. Если приложение росло много лет и использует собственную маршрутизацию, старую логику сессий или смешанные подходы, Codeception часто проще встроить.
Используйте browser tests для нескольких хрупких сценариев:
- вход и выход из системы
- одна форма с высокой ценностью
- один сценарий платежа или заказа
- одно административное действие, которым сотрудники пользуются каждый день
- одна страница со сложным JavaScript-поведением
Небольшого набора достаточно. Если тестовый набор пытается покрыть в браузере каждую ветку, люди перестают ему доверять, потому что он становится медленным и хрупким.
Выбирайте сценарии, которые действительно создают боль. Например, если старая форма заказа часто ломается после небольшого рефакторинга, добавьте один browser test, который заполнит форму, отправит её и проверит страницу успеха. Один такой тест может сэкономить часы ручной проверки и сделать рефакторинг legacy PHP намного спокойнее.
Простой способ добавлять тесты до рефакторинга
Старый PHP-код кажется менее страшным, когда вы сужаете цель. Выберите один class, один route или один экран, который часто меняется. Этого достаточно — например, итог checkout, callback входа или форма заказа.
Сначала проверьте то, как код работает сегодня, даже если вам это не нравится. Если route возвращает статус 200 и показывает "Order received" для известного запроса, зафиксируйте это первым. Такой smoke test не доказывает, что код хороший. Он доказывает, что вы заметите, когда его поведение изменится.
Небольшая форма заказа — хороший пример. Отправьте один валидный запрос и проверьте текущий ответ. Затем отправьте один сломанный запрос, например с отсутствующим email или просроченным купоном, и проверьте текущее сообщение об ошибке или код ответа.
Держите подготовку очень простой. Большинство инструментов тестирования PHP умеют загружать fixtures, заполнять test database или собирать фальшивый request, но для старта вам нужны лишь несколько случаев:
- одна fixture, которая должна пройти
- одна fixture, которая должна провалиться
- один edge case, который уже ломался раньше
- одно утверждение на основной вывод
- одно утверждение на побочный эффект, например сохранённую строку или отправленное событие
После этого измените только один метод, а не пять. Сразу снова запустите suite. Если smoke test падает, вы знаете, какое изменение это вызвало, и исправление обычно быстрое.
Вот то, что многие команды пропускают: остановитесь после небольшого шага. Зафиксируйте тест, зафиксируйте рефакторинг и переходите к следующему некрасивому методу уже завтра. Большие переписывания кажутся смелыми один день и мучительными целый месяц.
PHPUnit, Pest или другой runner одинаково хорошо поддерживают такой подход. Важен не столько инструмент, сколько цикл: зафиксировать текущее поведение, сделать одно изменение, прогнать тесты, повторить. Именно этот простой ритм делает рефакторинг legacy PHP возможным без гаданий.
Пример: приводим в порядок запутанную форму заказа
Грязная форма заказа обычно скрывает сразу три проблемы в одном месте: правила ценообразования, валидацию и checkout-поток. Если сначала изменить controller, можно сломать итоги, не учесть скидку или по ошибке заблокировать реального клиента. Самый безопасный шаг — зафиксировать текущее поведение до любой уборки.
Начните с нескольких integration tests вокруг итоговой суммы заказа. Возьмите реальные случаи: обычную корзину, корзину со скидочным кодом и корзину, которая должна остаться пустой. Добавьте и правила налога, особенно если приложение смешивает taxable и non-taxable товары. Пока не нужна идеальная полнота. Нужны достаточные доказательства того, что математика остаётся прежней, пока вы двигаете код.
Для такой работы простые fixtures экономят много времени. Сделайте небольшие переиспользуемые fixtures для:
- скидочных кодов с понятными правилами
- ставок налога для одного или двух регионов
- пустой корзины и корзины с двумя товарами
- записи клиента с отсутствующими полями checkout
Такой небольшой набор полезнее огромного фальшивого каталога. Он делает тесты читаемыми и упрощает понимание ошибок.
Потом добавьте два browser tests. Первый должен покрывать happy path: добавить товары, применить код, отправить checkout и подтвердить финальную сумму. Второй должен намеренно вызвать ошибку, например отсутствующий postal code или просроченный скидочный код, и проверить, что пользователь видит правильное сообщение, а данные корзины остаются на месте. В browser testing один хороший happy path и один сценарий с ошибкой часто успокаивают сильнее, чем двадцать unit-тестов.
Когда эти тесты проходят, вынесите валидацию из controller. Перенесите правила в request object, класс validator или в небольшой service, если в приложении нет подходящей возможности фреймворка. Первый рефакторинг должен быть скучным. Не меняйте логику цен и валидацию в одном и том же коммите.
Именно здесь инструменты тестирования PHP начинают отрабатывать свою цену. Код может ещё один день оставаться некрасивым. Сначала важно, чтобы итог заказа, ошибки checkout и базовый сценарий покупки продолжали работать после каждого маленького изменения.
Ошибки, которые повышают риск
Большинство рефакторингов идут неудачно по банальной причине: люди меняют слишком много сразу. Если вы переписываете и тесты, и production-код за один проход, вы теряете точку отсчёта. Когда тест начинает падать, уже невозможно понять, был ли старый код неправильным, новый код неправильным или новый тест изменил правила.
В старых приложениях часто есть странное поведение, на которое рассчитывают пользователи. Сначала зафиксируйте его, даже если оно выглядит грязно. Приводить всё в порядок стоит после того, как тесты покажут, что вы всё ещё понимаете, как код работает.
Даже хорошие инструменты тестирования PHP не помогут, если тесты никогда не касаются реального поведения. Набор, полностью построенный на mock-объектах, может оставаться зелёным, пока приложение ломается на реальном SQL-запросе, формате даты или плохом значении конфигурации. Мокайте то, что медленно или вне вашего контроля, но давайте важным частям приложения взаимодействовать друг с другом.
Большие fixtures создают другую слепую зону. Команды загружают полную дамп-базу с сотнями строк, хотя тесту нужны только клиент, заказ и один неудачный платёж. Три строки легче читать, быстрее запускать и намного проще понимать.
Браузерные тесты тоже часто используют не по назначению. Они отлично подходят для checkout, login и форм с большим количеством движущихся частей. Но это плохой выбор для простой налоговой математики, очистки строк или одного правила скидки. Если unit- или integration-тест могут поймать баг за две секунды, не стоит тратить две минуты на управление браузером.
Ещё одна ловушка — стремиться к полному покрытию до первого этапа уборки. Звучит безопасно, но часто откладывает единственную по-настоящему важную работу. Начните с кода, который собираетесь трогать, и с путей, которыми пользователи пользуются каждый день.
Маленький честный набор тестов лучше огромного набора, который скрывает риск. Если одна форма заказа ломается, когда в одном запросе встречаются quantity, coupon и shipping, напишите этот тест первым. Потом рефакторьте с ясной целью, а не с надеждой.
Быстрые проверки перед каждым merge
Перед каждым merge должен быть один простой ответ на вопрос: если это изменение сломает старое поведение, заметите ли вы это до того, как заметит пользователь? Даже лучшие инструменты тестирования PHP не помогают, если suite проходит только на одном ноутбуке или проверяет только счастливый путь.
Небольшой pre-merge-ритуал сильно снижает страх в legacy PHP-рефакторинге. Он также убирает частую ошибку, когда в merge попадает код, который "выглядит чище", но тихо меняет поведение приложения.
Перед merge остановитесь и проверьте такие вещи:
- Убедитесь, что хотя бы один тест падает при изменении старого поведения. Если правило скидки, обновление статуса или ошибка формы сегодня работает определённым образом, один тест должен ругаться, когда это поведение меняется.
- Запустите suite на чистой машине или попросите об этом коллегу. Если другому разработчику нужны особые шаги, скрытые env vars или локальные хаки для базы данных, тесты ещё не готовы.
- Проверьте fixtures на трёх типах ввода: пустой, нормальный и сломанный. Старые приложения часто ломаются в странных углах, а не в очевидном сценарии.
- Оставьте один browser test для сценария, который ломается чаще всего. Для многих команд это login, checkout, signup или запутанная форма заказа с условными полями.
- Опишите рефакторинг одной короткой фразой. "Вынес расчёт налога в один класс" — понятно. "Привёл в порядок несколько областей" обычно значит, что изменение разрослось слишком сильно.
Небольшой пример помогает. Допустим, вы тронули старую форму заказа. Unit-тесты могут покрывать расчёт цены, а fixture-данные — валидные и невалидные адреса. Но всё равно нужен один browser test, который отправляет полную форму, потому что JavaScript, состояние сессии и сообщения валидации часто ломаются вместе.
Эта проверка занимает несколько минут. Она может сэкономить часы работы по откату.
Если на все пять пунктов легко ответить, merge, скорее всего, достаточно безопасен. Если хотя бы один ответ кажется расплывчатым, остановитесь и сначала усилите именно этот участок.
Что делать, когда приложение сопротивляется каждому изменению
Когда простое переименование ломает checkout, email-квитанции и административную панель, перестаньте делать широкие изменения. Старые приложения не любят скорость. Обычно они успокаиваются, когда вы сужаете масштаб и делаете каждый шаг легко откатываемым.
Сначала нанесите на карту области, которые скорее всего сломаются, прежде чем двигать файлы или переименовывать services. В legacy PHP-рефакторинге рискованнее всего обычно то, что связано с деньгами, входом в систему, сторонними API, background jobs и старыми общими helper-функциями, которыми "все пользуются", но за которые никто не отвечает.
Простая карта полезнее длинного плана:
- отметьте пользовательские сценарии, которые приносят доход или создают обращения в поддержку
- запишите файлы, которые запускают письма, платежи, экспорты или cron-задачи
- отметьте, какие части покрыты тестами, а какие — нет
- решите, как будете откатывать каждое изменение до релиза
Последний пункт особенно важен. Если вы не можете сказать, как откатить изменение за пять минут, задача слишком большая.
Разбейте уборку на очень короткие задачи с понятными точками остановки. Одна задача может добавить тесты вокруг отправки формы. Следующая — спрятать старую глобальную функцию за маленьким классом. Только после этого переименовывайте service или переносите код в новую папку. Маленькие шаги кажутся медленными, но обычно экономят дни исправлений.
Попробуйте такое правило: каждая задача должна помещаться в одну ветку, один review и один deploy. Если изменение требует трёх команд, переписывания схемы и рискованного окна релиза, разделите его на более мелкие части.
Когда проблема шире, чем код
Иногда приложение сопротивляется каждому изменению, потому что код — лишь часть беспорядка. Правила продукта живут в документации поддержки, deploy проходят нестабильно, логи неполные, а никто не знает, какая серверная задача ещё важна. В такой момент одни только инструменты тестирования PHP проект не спасут.
Здесь может помочь fractional CTO. Хороший специалист выстроит работу в правильном порядке, уменьшит радиус поражения и задаст точки отката сразу и для кода, и для продукта, и для инфраструктуры. Это часто дешевле, чем просить небольшую команду угадывать путь через хрупкую систему.
Oleg Sotnikov консультирует стартапы и небольшие команды по практическому тестированию и более безопасным планам уборки кода. Его опыт охватывает software architecture, production infrastructure, CI/CD и AI-first development, поэтому он может помочь, когда в старом PHP-приложении технический долг смешан с рисками релиза и неясным поведением продукта.
Возможно, на следующей неделе приложение всё ещё будет выглядеть неаккуратно. И это нормально. Важно другое: каждое изменение должно быть ограниченным, протестированным и легко обратимым, если что-то пойдёт не так.