# Тестування в Symfony 2026: PHPUnit, KernelTestCase та функціональні тести > Тестування Symfony з PHPUnit 12, KernelTestCase та WebTestCase. Повний посібник з модульних, інтеграційних та функціональних тестів. - Published: 2026-07-15 - Updated: 2026-07-15 - Author: SharpSkill - Reading time: 5 min --- Тестування застосунків Symfony з використанням PHPUnit 12 забезпечує структурований підхід до перевірки поведінки застосунку — від модульних тестів до повних симуляцій HTTP-запитів. Тестова інфраструктура фреймворку тісно інтегрується з контейнером сервісів, що спрощує тестування сервісів, контролерів та операцій з базою даних при мінімальному обсязі допоміжного коду. > **Стек тестування в Symfony 7.3** > > Symfony 7.3 поставляється з підтримкою PHPUnit 12, покращеною ізоляцією тестів через `ResetInterface` та компонентом `CrawlerSelector` для більш читабельних assertions у функціональних тестах. `WebTestCase` тепер підтримує assertions HTTP/2 push з коробки. ## Модульні тести сервісів з PHPUnit 12 Модульні тести перевіряють окремі класи в ізоляції. Для сервісів Symfony, які не мають залежностей від контейнера, стандартні тести PHPUnit працюють без будь-якого залучення фреймворку. ```php // tests/Unit/Service/PriceCalculatorTest.php namespace App\Tests\Unit\Service; use App\Service\PriceCalculator; use PHPUnit\Framework\TestCase; final class PriceCalculatorTest extends TestCase { private PriceCalculator $calculator; protected function setUp(): void { // Create instance directly - no container needed $this->calculator = new PriceCalculator(vatRate: 0.20); } public function testCalculateTotalWithVat(): void { $result = $this->calculator->calculateTotal(100.00); // Assert exact decimal value with delta for float comparison $this->assertEqualsWithDelta(120.00, $result, 0.001); } public function testCalculateTotalWithZeroAmount(): void { $result = $this->calculator->calculateTotal(0.00); $this->assertEqualsWithDelta(0.00, $result, 0.001); } } ``` Чисті модульні тести виконуються швидше за інтеграційні та точно визначають джерело помилок. Їх слід застосовувати для класів з бізнес-логікою, що трансформує дані або виконує обчислення. ## Інтеграційні тести з KernelTestCase Коли сервіси залежать від контейнера Symfony — підключень до бази даних, адаптерів кешу або інших сервісів — `KernelTestCase` завантажує мінімальний kernel та надає доступ до контейнера сервісів. ```php // tests/Integration/Repository/UserRepositoryTest.php namespace App\Tests\Integration\Repository; use App\Entity\User; use App\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; final class UserRepositoryTest extends KernelTestCase { private EntityManagerInterface $em; private UserRepository $repository; protected function setUp(): void { // Boot kernel and fetch services from container self::bootKernel(); $container = static::getContainer(); $this->em = $container->get(EntityManagerInterface::class); $this->repository = $container->get(UserRepository::class); } public function testFindByEmailReturnsUserWhenExists(): void { // Arrange: create and persist test user $user = new User(); $user->setEmail('test@example.com'); $user->setPassword('hashed_password'); $this->em->persist($user); $this->em->flush(); // Act: query through repository $found = $this->repository->findByEmail('test@example.com'); // Assert: verify retrieval $this->assertNotNull($found); $this->assertSame('test@example.com', $found->getEmail()); } protected function tearDown(): void { // Clean up to prevent test pollution $this->em->createQuery('DELETE FROM App\Entity\User')->execute(); parent::tearDown(); } } ``` Метод `getContainer()` повертає тестовий контейнер, який відкриває приватні сервіси для тестування. Це відрізняється від робочого контейнера, де приватні сервіси залишаються недоступними. ## Ізоляція бази даних за допомогою транзакцій Ізоляція тестів запобігає впливу даних з одного тесту на інший. [DAMADoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle) обгортає кожен тест у транзакцію, яка автоматично відкочується. ```php // tests/Integration/Service/OrderServiceTest.php namespace App\Tests\Integration\Service; use App\Entity\Order; use App\Service\OrderService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; final class OrderServiceTest extends KernelTestCase { private OrderService $orderService; private EntityManagerInterface $em; protected function setUp(): void { self::bootKernel(); $container = static::getContainer(); $this->orderService = $container->get(OrderService::class); $this->em = $container->get(EntityManagerInterface::class); } public function testCreateOrderPersistsToDatabase(): void { // With DAMA bundle, this persists within a transaction $order = $this->orderService->create( customerId: 1, items: [['sku' => 'ABC123', 'quantity' => 2]] ); // Verify persistence $this->em->clear(); $persisted = $this->em->find(Order::class, $order->getId()); $this->assertNotNull($persisted); $this->assertCount(1, $persisted->getItems()); } // Transaction rolls back after test - no cleanup needed } ``` Бандл налаштовується у файлі `phpunit.xml.dist` додаванням розширення. Після цього кожен тест виконується в повній ізоляції без необхідності писати код очищення. ## Функціональні тести з WebTestCase Функціональні тести симулюють HTTP-запити та перевіряють відповіді. `WebTestCase` надає клієнт, який виконує запити до контролерів без запуску веб-сервера. ```php // tests/Functional/Controller/ProductControllerTest.php namespace App\Tests\Functional\Controller; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; final class ProductControllerTest extends WebTestCase { public function testListProductsReturnsJsonArray(): void { // Create client and make GET request $client = static::createClient(); $client->request('GET', '/api/products'); // Assert response status $this->assertResponseIsSuccessful(); $this->assertResponseStatusCodeSame(Response::HTTP_OK); // Assert JSON structure $this->assertResponseHeaderSame('Content-Type', 'application/json'); $data = json_decode($client->getResponse()->getContent(), true); $this->assertIsArray($data); $this->assertArrayHasKey('products', $data); } public function testCreateProductRequiresAuthentication(): void { $client = static::createClient(); $client->request('POST', '/api/products', [], [], [ 'CONTENT_TYPE' => 'application/json', ], json_encode(['name' => 'New Product', 'price' => 29.99])); // Unauthenticated requests should return 401 $this->assertResponseStatusCodeSame(Response::HTTP_UNAUTHORIZED); } } ``` Метод `$client->request()` приймає HTTP-метод, URI, параметри, файли, серверні змінні та вміст body. Symfony обробляє запит через повний kernel, включаючи middleware, безпеку та маршрутизацію. ## Тестування автентифікованих запитів Для endpoint-ів, що вимагають автентифікації, [компонент безпеки Symfony](https://symfony.com/doc/current/security.html) надає метод `loginUser()` на тестовому клієнті. ```php // tests/Functional/Controller/AdminControllerTest.php namespace App\Tests\Functional\Controller; use App\Entity\User; use App\Repository\UserRepository; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; final class AdminControllerTest extends WebTestCase { public function testAdminDashboardRequiresAdminRole(): void { $client = static::createClient(); $container = static::getContainer(); // Fetch a test user with ROLE_ADMIN $userRepository = $container->get(UserRepository::class); $adminUser = $userRepository->findOneBy(['email' => 'admin@example.com']); // Authenticate the client $client->loginUser($adminUser); // Access protected route $client->request('GET', '/admin/dashboard'); $this->assertResponseIsSuccessful(); $this->assertSelectorTextContains('h1', 'Admin Dashboard'); } public function testRegularUserCannotAccessAdmin(): void { $client = static::createClient(); $container = static::getContainer(); $userRepository = $container->get(UserRepository::class); $regularUser = $userRepository->findOneBy(['email' => 'user@example.com']); $client->loginUser($regularUser); $client->request('GET', '/admin/dashboard'); // Should be forbidden, not redirected to login $this->assertResponseStatusCodeSame(403); } } ``` Метод `loginUser()` налаштовує токен безпеки без проходження через форму входу, що прискорює тести та робить їх більш цілеспрямованими. ## Тестування форм та обробка CSRF Тестування форм вимагає обробки CSRF-токенів. Компонент crawler витягує елементи форми та надсилає їх з правильною обробкою токенів. ```php // tests/Functional/Controller/RegistrationControllerTest.php namespace App\Tests\Functional\Controller; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; final class RegistrationControllerTest extends WebTestCase { public function testRegistrationFormSubmission(): void { $client = static::createClient(); // Load the registration page $crawler = $client->request('GET', '/register'); // Extract the form and fill fields $form = $crawler->selectButton('Register')->form([ 'registration_form[email]' => 'newuser@example.com', 'registration_form[plainPassword]' => 'SecurePass123!', 'registration_form[agreeTerms]' => true, ]); // Submit - CSRF token is included automatically $client->submit($form); // Assert redirect after successful registration $this->assertResponseRedirects('/login'); // Follow redirect and verify flash message $client->followRedirect(); $this->assertSelectorTextContains('.alert-success', 'Registration successful'); } public function testRegistrationValidationErrors(): void { $client = static::createClient(); $crawler = $client->request('GET', '/register'); // Submit with invalid email $form = $crawler->selectButton('Register')->form([ 'registration_form[email]' => 'not-an-email', 'registration_form[plainPassword]' => '123', // Too short 'registration_form[agreeTerms]' => false, ]); $client->submit($form); // Should stay on same page with errors $this->assertResponseIsSuccessful(); $this->assertSelectorExists('.invalid-feedback'); } } ``` Метод `selectButton()` знаходить форму за текстом кнопки submit. Для форм без видимих кнопок слід використовувати `selectForm()` з CSS-селектором. ## Тестування консольних команд Команди часто виконують критичні операції, такі як імпорт даних чи заплановані завдання. `CommandTester` запускає команди та перехоплює їхній вивід. ```php // tests/Command/ImportUsersCommandTest.php namespace App\Tests\Command; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Tester\CommandTester; final class ImportUsersCommandTest extends KernelTestCase { public function testImportUsersWithValidCsv(): void { self::bootKernel(); $application = new Application(self::$kernel); $command = $application->find('app:import-users'); $tester = new CommandTester($command); // Execute with arguments and options $tester->execute([ 'file' => 'tests/fixtures/users.csv', '--dry-run' => true, ]); // Assert exit code (0 = success) $this->assertSame(0, $tester->getStatusCode()); // Assert output contains expected text $output = $tester->getDisplay(); $this->assertStringContainsString('Imported 5 users', $output); } public function testImportUsersFailsWithMissingFile(): void { self::bootKernel(); $application = new Application(self::$kernel); $command = $application->find('app:import-users'); $tester = new CommandTester($command); $tester->execute(['file' => 'nonexistent.csv']); // Non-zero exit code indicates failure $this->assertSame(1, $tester->getStatusCode()); $this->assertStringContainsString('File not found', $tester->getDisplay()); } } ``` Для команд з інтерактивним введенням слід передати `['interactive' => false]` як другий аргумент методу `execute()` або використати `setInputs()` для симуляції відповідей користувача. ## Тестування Messenger для асинхронних завдань Тестування обробників [Symfony Messenger](/technologies/symfony/interview-questions/messenger) вимагає перевірки того, що повідомлення відправляються коректно, а обробники обробляють їх згідно з очікуваннями. ```php // tests/Integration/Message/SendWelcomeEmailHandlerTest.php namespace App\Tests\Integration\Message; use App\Message\SendWelcomeEmail; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Transport\InMemoryTransport; final class SendWelcomeEmailHandlerTest extends KernelTestCase { public function testWelcomeEmailIsDispatched(): void { self::bootKernel(); $container = static::getContainer(); $bus = $container->get(MessageBusInterface::class); // Dispatch the message $bus->dispatch(new SendWelcomeEmail(userId: 123)); // Retrieve the in-memory transport configured in config/packages/test/messenger.yaml /** @var InMemoryTransport $transport */ $transport = $container->get('messenger.transport.async'); // Assert message was sent $this->assertCount(1, $transport->getSent()); $envelope = $transport->getSent()[0]; $message = $envelope->getMessage(); $this->assertInstanceOf(SendWelcomeEmail::class, $message); $this->assertSame(123, $message->getUserId()); } } ``` Необхідно налаштувати in-memory transport у файлі `config/packages/test/messenger.yaml` для перехоплення відправлених повідомлень без фактичної обробки чергою. ## Налаштування покриття коду Звіти [покриття коду PHPUnit](https://docs.phpunit.de/en/12.0/code-coverage.html) ідентифікують непротестовані шляхи коду. Налаштування покриття виконується у файлі `phpunit.xml.dist`. ```xml tests/Unit tests/Integration tests/Functional src src/DataFixtures src/Migrations ``` Тести з покриттям запускаються командою `php bin/phpunit --coverage-html var/coverage`. HTML-звіт показує статус покриття для кожного рядка коду. ## Висновок - Клас `TestCase` використовується для чистих модульних тестів без залежностей від контейнера - Клас `KernelTestCase` застосовується, коли тести потребують сервісів з контейнера - Клас `WebTestCase` служить для симуляції HTTP-запитів та assertions відповідей - [DAMADoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle) забезпечує автоматичний rollback транзакцій між тестами - Автентифікація тестується за допомогою `loginUser()` замість симуляції форм входу - `CommandTester` використовується для перевірки консольних команд з перехопленням введення/виведення - In-memory transports у тестовому середовищі дозволяють тестувати Messenger - Тести варто організовувати в каталоги Unit, Integration та Functional для ясності --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/symfony/symfony-testing-phpunit-kerneltestcase-functional-tests