Три базові класи поверх PHPUnit
Цей сайт написаний на Laravel, тому блоку з нашим кодом тут немає. Натомість поруч з кожною ідеєю Symfony стоїть її відповідник у Laravel: принципи ті самі, різняться назви.
Unit-тест у Symfony це звичайний PHPUnit\Framework\TestCase без ядра. Коли потрібні справжні сервіси, тест успадковує : self::bootKernel() піднімає ядро в середовищі test, а static::getContainer() дає контейнер. додає static::createClient(), тобто KernelBrowser, який передає Request прямо в ядро, без вебсервера й мережі. У Laravel обидві ролі виконує tests/TestCase.php: застосунок піднімається перед кожним тестом, а $this->get() робить те саме, що createClient()->request().
Сервіси в Symfony за замовчуванням приватні: з робочого контейнера їх не дістати через get(), а невикористані компілятор узагалі видаляє. static::getContainer() повертає , який бачить і приватні сервіси, що вціліли після компіляції, а через set() дозволяє підмінити сервіс, поки його ніхто не створив. У Laravel відповідник це $this->app->instance() або $this->mock().
Налаштування для тестів живуть у : phpunit.dist.xml ставить APP_ENV=test, змінні беруться з .env.test, а .env.local у цьому середовищі ігнорується, щоб тести поводились однаково в усіх. Конфіг пакетів для тестів пишуть у блоці when@test, і саме там вмикають framework.test: true, без якого немає ні клієнта, ні тестового контейнера. У Laravel ту саму роль грають теги env у phpunit.xml і файл .env.testing.
Побічні дії Symfony ловить своїми засобами. Messenger у тестах переводять на , і тест читає надіслані повідомлення через getSent(). Листи перевіряють assertEmailCount() і getMailerMessage(), а HTTP-клієнт підміняють MockHttpClient з MockResponse. Для бази є два сторонні бандли, яких у самому Symfony немає: загортає кожен тест у транзакцію й відкочує її, а Foundry (zenstruck/foundry) дає фабрики сутностей, схожі на фабрики моделей Laravel.
Куди входить тест у Symfony
- TestCase використовує ShippingRates (new).
- KernelTestCase використовує Kernel (bootKernel()).
- WebTestCase використовує KernelBrowser (createClient()).
- KernelBrowser використовує Kernel (Request).
- Kernel використовує test.service_container.
- test.service_container використовує InMemoryTransport (get()).
Symfony і Laravel поруч
Ті самі чотири сценарії двома фреймворками: сервіс з контейнера, сторінка вакансії, замовлення з чергою й листом, список вакансій з бази. Перемикайте пари вкладок. Для Symfony в when@test уже стоїть транспорт async: 'in-memory://' і підключено розширення DAMA та Foundry, для Laravel працює tests/Pest.php з RefreshDatabase.
Тестовий контейнер своїми руками
-
1
рядки 3–34
Клієнт Нової пошти за інтерфейсом: справжній ходить у мережу, фейк повертає заготовлену ціну. ShippingRates залежить лише від інтерфейсу.
-
2
рядки 36–66
Скомпільований контейнер. Сервіси приватні: get() їх не віддає, а створений сервіс уже не замінити.
-
3
рядки 68–82
Тестовий контейнер бачить і приватні сервіси. Так у Symfony працює test.service_container, який повертає static::getContainer().
-
4
рядки 84–93
Звичайний контейнер приватний ShippingRates не віддає. У Symfony так поводиться $kernel->getContainer()->get().
-
5
рядки 95–98
Підміна до першого використання: ShippingRates збирається вже з фейком, і запиту в мережу немає.
-
6
рядки 100–104
Клієнт уже створено, тож друга підміна падає. Справжній TestContainer::set() кидає такий самий виняток для вже ініціалізованого приватного сервісу.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
На чому спотикаються в тестах Symfony
bootKernel() перед createClient()
Тест спершу дістає сервіс з контейнера або створює сутність фабрикою, а вже потім викликає static::createClient(). Ядро на цей момент уже піднято, і WebTestCase кидає LogicException: Booting the kernel before calling createClient() is not supported.
Натомість: створюйте клієнт першим рядком тесту, а сервіси беріть після нього через static::getContainer(). Якщо підготовка мусить іти раніше, вимкніть ядро перед клієнтом: static::ensureKernelShutdown(), так радить і документація Foundry.
Підміна сервісу, що зникає після запиту
static::getContainer()->set() підмінив клієнт API, перший запит пройшов з фейком, а другий раптом пішов у мережу. KernelBrowser перед кожним наступним запитом перезавантажує ядро, і контейнер створюється заново.
Натомість: для кількох запитів з підміненим сервісом викличте $client->disableReboot() або задайте фейк конфігом у when@test, наприклад MockHttpClient через опцію mock_response_factory у налаштуваннях http_client.
Приватний сервіс, якого немає в тестовому контейнері
static::getContainer()->get(PriceFormatter::class) кидає ServiceNotFoundException, хоча клас зареєстровано. Сервіс ніхто не використовує, і компілятор його видалив, або вбудував в інший сервіс.
Натомість: тестуйте сервіс через того, хто ним користується, або створіть його в тесті через new. Робити сервіс public заради тесту не треба.
assertEmailCount() для листів через Messenger
Листи маршрутизовано в асинхронний транспорт, і assertEmailCount(1) показує нуль: Mailer вважає такий лист поставленим у чергу, а не надісланим.
Натомість: рахуйте assertQueuedEmailCount(), або в when@test надсилайте SendEmailMessage синхронно, якщо тесту потрібен сам лист.
Який базовий клас узяти
- KernelTestCase
- Базовий клас Symfony для тестів з ядром. bootKernel() створює ядро з KERNEL_CLASS у середовищі test, getContainer() дає тестовий контейнер, а після кожного тесту ядро вимикається.
- WebTestCase
- Нащадок KernelTestCase для функціональних тестів. createClient() повертає KernelBrowser, а трейти з асертами додають assertResponseIsSuccessful(), assertSelectorTextContains() і листи через assertEmailCount().
- Тестовий контейнер
- Сервіс test.service_container. Дає доступ до приватних сервісів, якщо компілятор їх не вбудував у інші й не видалив, і дозволяє підмінити сервіс через set(), доки той не створений.
- Середовище test
- Значення APP_ENV=test. Для нього Symfony читає .env.test і .env.test.local, пропускає .env.local і застосовує блоки when@test з конфігів.
- In-memory транспорт
- Транспорт Messenger з DSN in-memory://. Нікуди не відправляє повідомлення, а складає їх у памʼяті: getSent(), getAcknowledged(), getRejected().
- DAMA DoctrineTestBundle
- Сторонній бандл dama/doctrine-test-bundle. Розширення PHPUnit відкриває транзакцію перед кожним тестом і відкочує її після, тож базу не треба перестворювати між тестами.
Питання зі співбесід
KernelTestCase піднімає ядро в середовищі test і дає контейнер, щоб викликати сервіси напряму. WebTestCase його розширює: createClient() повертає KernelBrowser, який шле запити в ядро без вебсервера, і додає асерти відповіді та сторінки. У Laravel обидві ролі в одному TestCase: app() для сервісів і $this->get() для запитів.
static::getContainer() повертає тестовий контейнер test.service_container, який віддає й приватні сервіси. Не спрацює для сервісу, який компілятор видалив як невикористаний або вбудував в інший: його в контейнері вже немає. Потрібен framework.test: true, у тестовому середовищі його вмикає рецепт.
У when@test задати транспорту DSN in-memory://, а в тесті взяти його з тестового контейнера як messenger.transport.async і перевірити getSent(): кількість конвертів і клас повідомлення в getMessage(). Обробник не викликається, тому його тестують окремо. У Laravel те саме дає Queue::fake() чи Bus::fake().
Схема створюється один раз, а кожен тест іде в транзакції, яку потім відкочують, тож база після тесту чиста без DROP і міграцій. Не допоможе там, де транзакції немає або вона непомітно комітиться: DDL на кшталт ALTER TABLE в MySQL, другий процес чи зʼєднання, яке не бачить незакомічених даних, код, що сам керує транзакціями. Тоді лишається ResetDatabase з Foundry або окрема база.