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.

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.
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.
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.
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 opakowuje każdy test w transakcję, która automatycznie wykonuje rollback.
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.
Gotowy na rozmowy o Symfony?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
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.
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 udostępnia metodę loginUser() na kliencie testowym.
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.
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.
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 wymaga weryfikacji, że wiadomości są prawidłowo wysyłane, a handlery przetwarzają je zgodnie z oczekiwaniami.
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 identyfikują nieprzetestowane ścieżki kodu. Konfigurację pokrycia umieszcza się w pliku phpunit.xml.dist.
<!-- phpunit.xml.dist -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
executionOrder="depends,defects"
cacheResult="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="Functional">
<directory>tests/Functional</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
<exclude>
<directory>src/DataFixtures</directory>
<directory>src/Migrations</directory>
</exclude>
</source>
<coverage>
<report>
<html outputDirectory="var/coverage"/>
<clover outputFile="var/coverage/clover.xml"/>
</report>
</coverage>
</phpunit>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
TestCasesłuży do czystych testów jednostkowych bez zależności od kontenera - Klasa
KernelTestCasejest odpowiednia, gdy testy wymagają serwisów z kontenera - Klasa
WebTestCasesłuży do symulacji żądań HTTP i asercji odpowiedzi - DAMADoctrineTestBundle zapewnia automatyczny rollback transakcji między testami
- Uwierzytelnianie testuje się za pomocą
loginUser()zamiast symulowania formularzy logowania CommandTestersł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
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Udostępnij
Powiązane artykuły

Wstrzykiwanie zależności w Symfony: kontener usług, autowiring i tagi w 2026
Dogłębna analiza wstrzykiwania zależności w Symfony: kontener usług, autowiring, atrybut #[Autowire], wiązanie interfejsów, otagowane usługi z #[AutowireIterator], compiler passes i usługi leniwe w Symfony 7.4 i 8.0.

Symfony i Docker w 2026 roku: środowisko deweloperskie i wdrożenie produkcyjne
Kompletny przepływ pracy Symfony z Dockerem na 2026 rok: środowisko deweloperskie FrankenPHP, wieloetapowy obraz produkcyjny, tryb worker, szyfrowane sekrety, optymalizacja obrazu i wdrożenia bez przestojów wraz z migracjami.

Bezpieczeństwo Symfony w 2026: Voters, Firewalle i Pytania Rekrutacyjne
Kompleksowy przewodnik po systemie bezpieczeństwa Symfony: firewalle, votery, atrybut IsGranted, strategie decyzyjne, debugowanie w Twig 7.4 oraz pytania z rozmów kwalifikacyjnych dla programistów PHP.