Тестування в Symfony 2026: PHPUnit, KernelTestCase та функціональні тести
Тестування Symfony з PHPUnit 12, KernelTestCase та WebTestCase. Повний посібник з модульних, інтеграційних та функціональних тестів.

Тестування застосунків Symfony з використанням PHPUnit 12 забезпечує структурований підхід до перевірки поведінки застосунку — від модульних тестів до повних симуляцій HTTP-запитів. Тестова інфраструктура фреймворку тісно інтегрується з контейнером сервісів, що спрощує тестування сервісів, контролерів та операцій з базою даних при мінімальному обсязі допоміжного коду.
Symfony 7.3 поставляється з підтримкою PHPUnit 12, покращеною ізоляцією тестів через ResetInterface та компонентом CrawlerSelector для більш читабельних assertions у функціональних тестах. WebTestCase тепер підтримує assertions HTTP/2 push з коробки.
Модульні тести сервісів з PHPUnit 12
Модульні тести перевіряють окремі класи в ізоляції. Для сервісів Symfony, які не мають залежностей від контейнера, стандартні тести PHPUnit працюють без будь-якого залучення фреймворку.
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 та надає доступ до контейнера сервісів.
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 обгортає кожен тест у транзакцію, яка автоматично відкочується.
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 додаванням розширення. Після цього кожен тест виконується в повній ізоляції без необхідності писати код очищення.
Готовий до співбесід з Symfony?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Функціональні тести з WebTestCase
Функціональні тести симулюють HTTP-запити та перевіряють відповіді. WebTestCase надає клієнт, який виконує запити до контролерів без запуску веб-сервера.
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 надає метод loginUser() на тестовому клієнті.
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 витягує елементи форми та надсилає їх з правильною обробкою токенів.
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 запускає команди та перехоплює їхній вивід.
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 вимагає перевірки того, що повідомлення відправляються коректно, а обробники обробляють їх згідно з очікуваннями.
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 ідентифікують непротестовані шляхи коду. Налаштування покриття виконується у файлі 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>Тести з покриттям запускаються командою php bin/phpunit --coverage-html var/coverage. HTML-звіт показує статус покриття для кожного рядка коду.
Висновок
- Клас
TestCaseвикористовується для чистих модульних тестів без залежностей від контейнера - Клас
KernelTestCaseзастосовується, коли тести потребують сервісів з контейнера - Клас
WebTestCaseслужить для симуляції HTTP-запитів та assertions відповідей - DAMADoctrineTestBundle забезпечує автоматичний rollback транзакцій між тестами
- Автентифікація тестується за допомогою
loginUser()замість симуляції форм входу CommandTesterвикористовується для перевірки консольних команд з перехопленням введення/виведення- In-memory transports у тестовому середовищі дозволяють тестувати Messenger
- Тести варто організовувати в каталоги Unit, Integration та Functional для ясності
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Поділитися
Пов'язані статті

Впровадження залежностей у Symfony: контейнер сервісів, autowiring і теги 2026
Поглиблений розбір впровадження залежностей у Symfony: контейнер сервісів, autowiring, атрибут #[Autowire], прив'язування інтерфейсів, теговані сервіси з #[AutowireIterator], compiler passes та ліниві сервіси в Symfony 7.4 і 8.0.

Symfony і Docker у 2026 році: середовище розробки та production-деплой
Повний робочий процес Symfony Docker для 2026 року: середовище розробки на FrankenPHP, багатоетапний production-образ, режим воркера, зашифровані секрети, оптимізація образу та деплой без простою з міграціями.

Безпека Symfony у 2026 році: Voters, Firewalls та питання технічних співбесід
Вичерпний посібник із системи безпеки Symfony: firewalls, voters, атрибут IsGranted, стратегії прийняття рішень, дебагінг у Twig 7.4 та питання технічних співбесід для PHP-розробників.