Привет! Недавно я завершил обновление фреймворка блога с Laravel 12 до 13й версии. Обновление мажорной версии всегда таит в себе сюрпризы и этот раз не был исключением. Расскажу, что пошло не так, почему и пути решения.
Что и зачем обновлял
Приложение работает на стеке: Laravel + Livewire 4 + Flux UI + Pest 4. С выходом новой стабильной версии обновление - плановый шаг, позволяющий получить последние улучшения фреймворка и продолжить разработку на последней актуальной поддерживаемой версии.
Сам процесс обновления composer.json прошёл без неожиданностей. Сложности начались, когда пришло время запуска финальных тестов.
17 упавших тестов
После обновления зависимостей тест-сьют показал 17 красных крестиков. Разберём каждую группу проблем.
Volt::test() больше не существует
Самое массовое изменение. Пакет pestphp/pest-plugin-laravel версии 4.1 убрал глобальный класс Volt для тестирования. Все тесты, которые использовали Volt::test(...), сломались с ошибкой Class "Volt" not found.
Решение: заменить везде на Livewire::test(...) , по факту Volt-компоненты и есть Livewire-компоненты.
// Было
Volt::test('posts.form')->...
// Стало
Livewire::test('posts.form')->...Модуль "Цензура" перестал обнаруживать в вводимых данных запрещённые слова
Тест цензуры в основном проверял что при сохранении поста или комментария с запрещённым словом запись отклоняется. Тесты стали проваливаться - наблюдатели (PostObserver, CommentObserver) перестали срабатывать корректно.
Причина: в приложении используется трейт Translatable, который переопределяет аксессоры content и title. При обращении к $post->content трейт автоматически переводил текст через внешний сервис, используя текущую локаль приложения (APP_LOCALE=ru). В итоге английское слово badword превращалось в русский перевод и цензор его попросту не находил.
Решение: в наблюдателях обращаться к сырым атрибутам в обход аксессоров:
// Было — трейт переводил текст
$result = $this->censorshipService->scan($post->content, ...);
// Стало — берём значение напрямую из атрибутов модели
$result = $this->censorshipService->scan(
$post->getAttributes()['content'] ?? '', ...
);Ошибки валидации исчезали до того, как тест их проверял
Тесты аутентификации проверяли, что при неверных данных в сессии появляются ошибки. Тесты падали, хотя логика контроллера не менялась.
Причина: оказалось, что ThemeMiddleware вызывал Session::save() после завершения запроса. В Laravel сессия сохраняется один раз - в конце цикла обработки запроса и при этом "стареют" (aging) и flash-данные: переносит их из new в old. Повторный вызов Session::save() в middleware состарил данные второй раз — и ошибки валидации, попавшие в flash, удалялись раньше, чем их успевал прочитать тест.
Решение: убрать явный вызов Session::save() из ThemeMiddleware. Laravel сам управляет жизненным циклом сессии.
// Удалили из ThemeMiddleware:
Session::save();Неправильный метод для проверки ошибок
В AuthenticationTest использовался метод assertSessionHasErrorsIn('email'), который проверяет именованный error bag с именем email. Но Fortify кладёт ошибки в дефолтный bag.
// Было — ищет named bag 'email' (которого нет)
$response->assertSessionHasErrorsIn('email');
// Стало — ищет в default bag
$response->assertSessionHasErrors(['email']);Непроверенные email-адреса блокировали доступ
Несколько тестов создавали пользователей и сразу проверяли их доступ к защищённым маршрутам. Фабрика UserFactory с вероятностью 40% создаёт пользователей без подтверждённого email и тогда middleware verified перенаправлял их на страницу верификации вместо нужной страницы. Это не было новой проблемой фреймворка - просто непредсказуемость в выполнении теста, которую раньше не замечал. Обновление дало повод навести порядок.
Решение: явно указывать email_verified_at там, где это важно:
$user = User::factory()->create(['email_verified_at' => now()]);Некорректная работа поиска по постам
PostSearchTest создавал посты через фабрику по умолчанию, а по умолчанию фабрика создаёт посты со статусом draft. Главная страница и страница поиска показывают только опубликованные посты, поэтому тесты неизменно получали пустую выдачу.
// Было
Post::factory()->create(['title' => 'Laravel Best Practices', ...]);
// Стало
Post::factory()->published()->create(['title' => 'Laravel Best Practices', ...]);Значения счётчика просмотров не менялись при просмотре/чтении поста
PostViewCountTest падал так как view_count оставался равным 0 после вызова recordView().
Причина: в PostViewService использовалось деструктурирование результата firstOrCreate():
[$view, $created] = $post->postViews()->firstOrCreate(...);
Метод HasOneOrMany::firstOrCreate() возвращает одну модель, а не кортеж. $created оказывался null, блок if ($created) не выполнялся, и increment('view_count') никогда не вызывался.
Решение: использовать свойство wasRecentlyCreated:
$view = $post->postViews()->firstOrCreate(...);
if ($view->wasRecentlyCreated) {
$post->increment('view_count');
dispatch(new ResolveViewLocationJob($view->id, $ipAddress));
}Конфликт локалей
Приложение поддерживает два языка (EN/RU). В тестах обнаружился конфликт: одни тесты ожидали русские строки (Предпросмотр, Панель управления), другие - английские (Two Factor Authentication, Disabled). При любой фиксированной локали часть тестов падала.
Решение в два шага:
1. Зафиксировать APP_LOCALE=en в phpunit.xml - это даёт предсказуемое окружение для всего тест-сьюта.
2. Обновить тесты, которые полагались на хардкоженные строки на русском, - заменить их на вызовы __():
// Было
$response->assertSee('Предпросмотр')->assertSee('Панель');
// Стало
$response->assertSee(__('ui.Preview'))->assertSee(__('ui.Dashboard'));Нехватка памяти при прогоне полного сьюта
PHP падал с Allowed memory size exhausted при запуске всех тестов подряд. Виновник - пакет aws/aws-sdk-php, который при загрузке читает огромный файл данных для S3 endpoint-rules.
Решение: увеличить лимит памяти в phpunit.xml: <ini name="memory_limit" value="512M"/>
Как видим большинство проблем не были напрямую связаны с изменениями в Laravel 13 - это накопленный технический долг, который обновление просто сделало видимым. Пара исключений: удаление Volt::test() из pest-plugin-laravel и изменение поведения деструктурирования результата firstOrCreate() на relation-объекте.
Важная мысль: хороший тест-сьют - лучший союзник при обновлениях. Без него половина этих проблем ушла бы тихо в продакшн.
Happy codding! и до скорого!

Станьте первым, кто прокомментирует эту запись