# Testowanie w Symfony 2026: PHPUnit, KernelTestCase i testy funkcjonalne > Testowanie aplikacji Symfony z PHPUnit 12, KernelTestCase i WebTestCase. Kompletny przewodnik po testach jednostkowych, integracyjnych i funkcjonalnych. - Published: 2026-07-15 - Updated: 2026-07-15 - Author: SharpSkill - Reading time: 5 min --- Testowanie aplikacji Symfony z wykorzystaniem PHPUnit 12 zapewnia uporządkowane podejście do weryfikacji zachowania aplikacji — od testów jednostkowych po pełne symulacje żądań HTTP. Infrastruktura testowa frameworka ściśle integruje się z kontenerem usług, co umożliwia łatwe testowanie serwisów, kontrolerów i operacji bazodanowych przy minimalnej ilości kodu pomocniczego. > **Stos testowy w Symfony 7.3** > > Symfony 7.3 oferuje wsparcie dla PHPUnit 12, ulepszoną izolację testów poprzez `ResetInterface` oraz komponent `CrawlerSelector` umożliwiający bardziej czytelne asercje w testach funkcjonalnych. `WebTestCase` obsługuje teraz natywnie asercje HTTP/2 push. ## Testy jednostkowe serwisów z PHPUnit 12 Testy jednostkowe weryfikują poszczególne klasy w izolacji. Dla serwisów Symfony, które nie mają zależności od kontenera, standardowe testy PHPUnit działają bez jakiegokolwiek zaangażowania frameworka. ```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); } } ``` Czyste testy jednostkowe wykonują się szybciej niż testy integracyjne i precyzyjnie wskazują źródło błędów. Należy je stosować dla klas zawierających logikę biznesową transformującą dane lub wykonującą obliczenia. ## Testy integracyjne z KernelTestCase Gdy serwisy zależą od kontenera Symfony — połączeń bazodanowych, adapterów cache lub innych serwisów — `KernelTestCase` uruchamia minimalny kernel i zapewnia dostęp do kontenera usług. ```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(); } } ``` Metoda `getContainer()` zwraca kontener testowy, który udostępnia prywatne serwisy do celów testowych. Różni się to od kontenera produkcyjnego, gdzie prywatne serwisy pozostają niedostępne. ## Izolacja bazy danych z wykorzystaniem transakcji Izolacja testów zapobiega wpływowi danych z jednego testu na inny. [DAMADoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle) opakowuje każdy test w transakcję, która automatycznie wykonuje rollback. ```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 } ``` Konfiguracja bundla w pliku `phpunit.xml.dist` polega na dodaniu rozszerzenia. Każdy test wykonuje się wtedy w pełnej izolacji bez konieczności pisania kodu czyszczącego. ## Testy funkcjonalne z WebTestCase Testy funkcjonalne symulują żądania HTTP i weryfikują odpowiedzi. `WebTestCase` zapewnia klienta wykonującego żądania do kontrolerów bez uruchamiania serwera web. ```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); } } ``` Metoda `$client->request()` przyjmuje metodę HTTP, URI, parametry, pliki, zmienne serwera i zawartość body. Symfony przetwarza żądanie przez pełny kernel, włączając middleware, bezpieczeństwo i routing. ## Testowanie żądań uwierzytelnionych Dla endpointów wymagających uwierzytelnienia [komponent bezpieczeństwa Symfony](https://symfony.com/doc/current/security.html) udostępnia metodę `loginUser()` na kliencie testowym. ```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); } } ``` Metoda `loginUser()` konfiguruje token bezpieczeństwa bez przechodzenia przez formularz logowania, co przyspiesza testy i czyni je bardziej ukierunkowanymi. ## Testowanie formularzy i obsługa CSRF Testowanie formularzy wymaga obsługi tokenów CSRF. Komponent crawler wyodrębnia elementy formularza i przesyła je z prawidłową obsługą tokenów. ```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'); } } ``` Metoda `selectButton()` znajduje formularz po tekście przycisku submit. Dla formularzy bez widocznych przycisków należy użyć `selectForm()` z selektorem CSS. ## Testowanie poleceń konsolowych Polecenia często wykonują krytyczne operacje, takie jak importy danych czy zadania zaplanowane. `CommandTester` uruchamia polecenia i przechwytuje ich wyjście. ```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()); } } ``` Dla poleceń z interaktywnym wejściem należy przekazać `['interactive' => false]` jako drugi argument metody `execute()` lub użyć `setInputs()` do symulowania odpowiedzi użytkownika. ## Testowanie Messenger dla zadań asynchronicznych Testowanie handlerów [Symfony Messenger](/technologies/symfony/interview-questions/messenger) wymaga weryfikacji, że wiadomości są prawidłowo wysyłane, a handlery przetwarzają je zgodnie z oczekiwaniami. ```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()); } } ``` Należy skonfigurować transport in-memory w pliku `config/packages/test/messenger.yaml`, aby przechwytywać wysyłane wiadomości bez faktycznego przetwarzania przez kolejkę. ## Konfiguracja pokrycia kodu Raporty [pokrycia kodu PHPUnit](https://docs.phpunit.de/en/12.0/code-coverage.html) identyfikują nieprzetestowane ścieżki kodu. Konfigurację pokrycia umieszcza się w pliku `phpunit.xml.dist`. ```xml tests/Unit tests/Integration tests/Functional src src/DataFixtures src/Migrations ``` Testy z pokryciem uruchamia się poleceniem `php bin/phpunit --coverage-html var/coverage`. Raport HTML pokazuje status pokrycia dla każdej linii kodu. ## Podsumowanie - Klasa `TestCase` służy do czystych testów jednostkowych bez zależności od kontenera - Klasa `KernelTestCase` jest odpowiednia, gdy testy wymagają serwisów z kontenera - Klasa `WebTestCase` służy do symulacji żądań HTTP i asercji odpowiedzi - [DAMADoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle) zapewnia automatyczny rollback transakcji między testami - Uwierzytelnianie testuje się za pomocą `loginUser()` zamiast symulowania formularzy logowania - `CommandTester` służy do weryfikacji poleceń konsolowych z przechwytywaniem wejścia/wyjścia - Transporty in-memory w środowisku testowym umożliwiają testowanie Messenger - Testy warto organizować w katalogi Unit, Integration i Functional dla przejrzystości --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/symfony/symfony-testing-phpunit-kerneltestcase-functional-tests