Locale
Обновление Laravel 12 → 13: реальный опыт и сломанные тесты
Артур Хайбуллин
Артур Хайбуллин 30 мар 2026, 23:50

Привет! Недавно я завершил обновление фреймворка блога с Laravel 12 до 13й версии. Обновление мажорной версии всегда таит в себе сюрпризы и этот раз не был исключением. Расскажу, что пошло не так, почему и пути решения.

1_xYVKMqq2A-zwQ-eoXscVXg.png

Что и зачем обновлял

Приложение работает на стеке: 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! и до скорого!

Комментарии (0)
Комментировать
Пока нет комментариев

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