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

Тесты, которые есть, и тесты, которые ловят проблемы, — разные вещи. Приложение на Symfony 7.x проходит CI зелёным, а через два дня после релиза отдаёт 500 на форме заказа, потому что тест мокал сервис оплаты и ни разу не касался реальной сериализации DTO. Ниже — чек-лист того, что стоит проверить в тестовой части перед выкаткой, с акцентом на места, где Symfony-проекты чаще всего протекают: изоляция базы между тестами, граница между юнит- и функциональным тестом, внешние сервисы и переменные окружения.
Статья про то, что и как ревьюить в тестах, а не про то, как написать первый тест. Если PHPUnit ещё не подключён — сначала composer require --dev symfony/test-pack, потом сюда.
Границы: что покрываете юнитом, а что — функционально
Первая типичная ошибка — всё писать как функциональные тесты через WebTestCase. Они гоняют весь ядро фреймворка, поднимают контейнер, дёргают базу и потому медленные. Обратная крайность — юнитом мокать всё подряд, включая Doctrine и сериализатор, после чего тест проходит, а прод падает на реальном маппинге.
Ориентир по типам тестов в Symfony:
| Тип | Базовый класс | Что проверяет | Скорость |
|---|---|---|---|
| Юнит | TestCase | Чистую логику: расчёты, валидаторы, value-объекты. Без контейнера и базы | Миллисекунды |
| Интеграционный | KernelTestCase | Сервисы из контейнера, репозитории на реальной базе, шину сообщений | Десятки мс |
| Функциональный | WebTestCase | HTTP-запрос → контроллер → ответ, редиректы, коды статусов, формы | Сотни мс |
Проверка на ревью: если тест мокает EntityManagerInterface и при этом проверяет, что данные корректно сохранились, — он проверяет мок, а не сохранение. Такой тест переводите на KernelTestCase с реальной тестовой базой.
Мок внешней системы проверяет ваш контракт с ней. Мок собственной инфраструктуры — Doctrine, сериализатора, контейнера — чаще всего проверяет, что вы правильно написали мок.
Что мокать точно стоит
- HTTP-клиенты к внешним API (платёжки, доставка) — через
MockHttpClientизsymfony/http-client; - отправку писем — Symfony Mailer в
test-окружении не шлёт реально, есть ассерты вродеassertEmailCount; - текущее время, если логика зависит от дат, — иначе тест сломается в полночь или в другой год.
Изоляция базы между тестами
Самый частый источник плавающих тестов — данные, которые один тест оставил другому. Тест A создал пользователя, тест B посчитал SELECT COUNT и упал, потому что ожидал ноль. При запуске поодиночке оба зелёные, вместе — красные.
Рабочий подход в экосистеме Symfony — пакет dama/doctrine-test-bundle. Он оборачивает каждый тест в транзакцию и откатывает её после — база возвращается в исходное состояние без пересоздания схемы. Подключение в phpunit.dist.xml через extension этого бандла (точное имя расширения смотрите в README пакета вашей версии, оно менялось между мажорными релизами PHPUnit).
Что проверить по изоляции:
- Каждый тестовый класс не зависит от порядка запуска. Запустите с
--order-by=random— если что-то отвалилось, тесты делят состояние. - Тестовая база отдельна от dev. В
.env.testуDATABASE_URLдругое имя базы (обычно с суффиксом_test). - Фикстуры грузятся в изолированном контексте, а не накапливаются. Если используете
doctrine/doctrine-fixtures-bundle, очистка перед загрузкой должна быть включена.
Если dama/doctrine-test-bundle по каким-то причинам не подходит (например, тест намеренно проверяет поведение при коммите), тогда база пересоздаётся или чистится вручную в setUp — но это медленнее на порядок.
Внешние сервисы и HTTP-клиент
Тест, который реально ходит в чужой API, — не тест, а лотерея: сеть, лимиты, чужой даунтайм. Symfony даёт MockHttpClient, который отдаёт заранее заданные ответы без сети.
<?php
// Фрагмент теста, Symfony 7.x, symfony/http-client
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
// Готовим ответ платёжного шлюза, который ждёт наш код
$paymentResponse = new MockResponse(
json_encode(['status' => 'succeeded', 'id' => 'pay_123']),
['http_code' => 200]
);
$httpClient = new MockHttpClient($paymentResponse);
// Внедряем мок вместо реального клиента и вызываем свой сервис
$paymentService = new PaymentService($httpClient);
$result = $paymentService->charge(1500);
$this->assertSame('succeeded', $result->getStatus());
Проверка на ревью: убедитесь, что мок отвечает не только на успешный сценарий. Отдельный тест должен подавать http_code => 500 или таймаут и проверять, что ваш код не роняет всё приложение, а обрабатывает ошибку.
Подмена сервиса в контейнере для функционального теста
В WebTestCase реальный контроллер достаёт зависимости из контейнера. Чтобы подставить мок, используйте контейнер тестового клиента: получаете его через static::getContainer() и подменяете сервис до запроса. Точный способ подмены (в частности, работа с приватными сервисами через тестовый контейнер) описан в разделе документации Symfony про тестирование — синтаксис зависит от версии, сверьтесь с ней, а не копируйте старые примеры со Stack Overflow.
Окружение и конфигурация теста
Тесты запускаются в окружении test, у которого свой набор переменных из .env.test. Здесь прячется отдельный класс багов: локально всё зелёное, в CI падает, потому что переменная объявлена только на машине разработчика.
- Все переменные, от которых зависят тесты, лежат в
.env.testили.env.test.local, а не только в вашей локальной среде; - секретов в
.env.testнет — вместо реального ключа платёжки там заглушка вродеYOUR_API_KEY, потому что тесты его не должны использовать по-настоящему; - в
testотключены реальные внешние транспорты: Mailer не шлёт, Messenger по умолчанию может работать синхронно или черезin-memoryтранспорт — проверьтеconfig/packages/messenger.yamlдля окружения test.
Чего в CI-прогоне часто не хватает
Зелёный PHPUnit — не гарантия рабочего релиза. Перед выкаткой добавьте в конвейер проверки, которые ловят то, что тесты пропускают:
- Валидация схемы Doctrine. Команда
doctrine:schema:validateловит рассинхрон между маппингом сущностей и реальной структурой базы — частую причину падений после миграций. - Прогон миграций на чистой базе. Отдельным шагом накатите все миграции с нуля на пустую тестовую базу. Ловит миграцию, которая работает поверх существующих данных, но падает на чистой.
- Проверка контейнера.
lint:containerнаходит несуществующие или неправильно объявленные сервисы до того, как контроллер попытается их получить в проде. - Линт конфигов и шаблонов.
lint:yamlиlint:twigловят синтаксис в файлах, которые unit-тесты не открывают.
Все четыре команды дешёвые и запускаются за секунды. Если хоть одна раньше не стояла в CI — добавьте, они закрывают целый класс ошибок, которые иначе всплывут только на боевом сервере.
Что делать дальше
Начните с малого и проверяемого: включите --order-by=random в один прогон и посмотрите, не рассыпались ли тесты — это покажет реальный уровень изоляции. Затем добавьте в CI четыре lint-проверки из последнего раздела. После этого стоит посмотреть на покрытие не как на процент, а как на карту: какие контроллеры и сервисы, критичные для денег и данных, вообще не заходят в тесты. Для замера — --coverage-text с включённым Xdebug или PCOV; проценты сами по себе врут, а список непокрытых критичных путей — нет.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: Symfony — Testing (официальная документация), dama/doctrine-test-bundle (GitHub)
Частые вопросы
У меня Symfony 6.4, а не 7.x — примеры подойдут?
Тесты зелёные поодиночке, но падают все вместе — в чём дело?
Обязательно ли мокать базу данных в тестах?
Что делать, если MockHttpClient не подхватывается в функциональном тесте?
Нужно ли гнать миграции в CI, если тесты и так проходят?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.