การทดสอบ Symfony ปี 2026: PHPUnit, KernelTestCase และ Functional Tests
คู่มือฉบับสมบูรณ์สำหรับการทดสอบแอปพลิเคชัน Symfony ด้วย PHPUnit 12, KernelTestCase สำหรับ integration testing และ WebTestCase สำหรับ functional tests ตาม best practices

การทดสอบ Symfony ด้วย PHPUnit 12 มอบแนวทางที่มีโครงสร้างสำหรับการตรวจสอบพฤติกรรมของแอปพลิเคชัน ตั้งแต่ unit test ไปจนถึงการจำลอง HTTP request แบบเต็มรูปแบบ โครงสร้างพื้นฐานการทดสอบของ framework ผสานรวมอย่างแน่นแฟ้นกับ service container ทำให้การทดสอบ service, controller และการดำเนินการ database เป็นเรื่องง่ายโดยมี boilerplate น้อยที่สุด
Symfony 7.3 มาพร้อมกับการรองรับ PHPUnit 12, การปรับปรุง test isolation ผ่าน ResetInterface และ component CrawlerSelector สำหรับการเขียน assertion ใน functional test ที่อ่านง่ายขึ้น WebTestCase รองรับ assertion HTTP/2 push ตั้งแต่เริ่มต้น
Unit Testing Services ด้วย PHPUnit 12
Unit test ตรวจสอบ class แต่ละตัวแยกกัน สำหรับ Symfony service ที่ไม่มี dependency กับ container, test PHPUnit มาตรฐานสามารถทำงานได้โดยไม่ต้องใช้ framework
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);
}
}Unit test บริสุทธิ์ทำงานเร็วกว่า integration test และสามารถระบุตำแหน่งที่เกิดข้อผิดพลาดได้อย่างแม่นยำ ควรใช้สำหรับ class ที่มี business logic ในการแปลงข้อมูลหรือทำการคำนวณ
Integration Testing ด้วย KernelTestCase
เมื่อ service ต้องพึ่งพา Symfony container ไม่ว่าจะเป็นการเชื่อมต่อ database, cache adapter หรือ service อื่นๆ KernelTestCase จะ boot kernel แบบน้อยที่สุดและให้การเข้าถึง service container
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() คืนค่า container สำหรับ test ซึ่งเปิดเผย private service สำหรับวัตถุประสงค์ในการทดสอบ สิ่งนี้แตกต่างจาก runtime container ที่ private service ยังคงไม่สามารถเข้าถึงได้
การแยก Database ด้วย Transactions
Test isolation ป้องกันไม่ให้ข้อมูลจาก test หนึ่งส่งผลกระทบต่อ test อื่น DAMADoctrineTestBundle ห่อแต่ละ test ใน transaction ที่ 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
}กำหนดค่า bundle ใน phpunit.xml.dist โดยการเพิ่ม extension แต่ละ test จะทำงานในการแยกอย่างสมบูรณ์โดยไม่ต้องมีโค้ด cleanup แบบ manual
พร้อมที่จะพิชิตการสัมภาษณ์ Symfony แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Functional Testing ด้วย WebTestCase
Functional test จำลอง HTTP request และตรวจสอบ response WebTestCase ให้ client ที่ส่ง request ไปยัง controller โดยไม่ต้องเริ่มต้น web server
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 method, URI, parameters, files, server variables และ body content Symfony ประมวลผล request ผ่าน kernel ทั้งหมด รวมถึง middleware, security และ routing
การทดสอบ Request ที่ต้องยืนยันตัวตน
สำหรับ endpoint ที่ต้องการการยืนยันตัวตน security component ของ Symfony มีเมธอด loginUser() บน test client
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() ตั้งค่า security token โดยไม่ต้องผ่านฟอร์มเข้าสู่ระบบ ทำให้ test เร็วขึ้นและมุ่งเน้นมากขึ้น
การทดสอบ Form และการจัดการ CSRF
การทดสอบ form ต้องจัดการกับ token CSRF Component crawler ดึงเอา element ของ form และ submit พร้อมกับการจัดการ token ที่เหมาะสม
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() ค้นหา form จากข้อความของปุ่ม submit สำหรับ form ที่ไม่มีปุ่มที่มองเห็น ให้ใช้ selectForm() กับ CSS selector
การทดสอบ Console Commands
Command มักดำเนินการที่สำคัญเช่น import ข้อมูลหรือ scheduled jobs CommandTester execute command และ capture output
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());
}
}สำหรับ command ที่มี input แบบ interactive ให้ส่ง ['interactive' => false] เป็น argument ที่สองให้กับ execute() หรือใช้ setInputs() เพื่อจำลองการตอบกลับของผู้ใช้
การทดสอบ Messenger สำหรับ Async Jobs
การทดสอบ handler ของ Symfony Messenger ต้องตรวจสอบว่า message ถูก dispatch อย่างถูกต้องและ handler ประมวลผลตามที่คาดหวัง
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 เพื่อ capture message ที่ถูก dispatch โดยไม่ต้องประมวลผล queue จริง
การกำหนดค่า Code Coverage
รายงาน code coverage ของ PHPUnit ระบุเส้นทางโค้ดที่ยังไม่ได้ทดสอบ กำหนดค่า coverage ใน 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>รัน test พร้อม coverage โดยใช้ php bin/phpunit --coverage-html var/coverage รายงาน HTML แสดงสถานะ coverage ทีละบรรทัด
สรุป
- ใช้
TestCaseสำหรับ unit test บริสุทธิ์ที่ไม่มี dependency กับ container - ใช้
KernelTestCaseเมื่อ test ต้องการ service จาก container - ใช้
WebTestCaseสำหรับการจำลอง HTTP request และ assertion ของ response - กำหนดค่า DAMADoctrineTestBundle สำหรับ rollback transaction อัตโนมัติระหว่าง test
- ทดสอบการยืนยันตัวตนด้วย
loginUser()แทนที่จะจำลองฟอร์มเข้าสู่ระบบ - ใช้
CommandTesterสำหรับการตรวจสอบ console command พร้อม capture input/output - กำหนดค่า in-memory transport ใน environment ของ test สำหรับการทดสอบ Messenger
- จัดระเบียบ test ลงในไดเรกทอรี Unit, Integration และ Functional เพื่อความชัดเจน
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Symfony Live Components และ UX 3.0: แอปพลิเคชันแบบ Reactive โดยไม่ต้องใช้ JavaScript ในปี 2026
Symfony Live Components สร้างอินเทอร์เฟซแบบ reactive ด้วย PHP และ Twig โดยไม่ต้องใช้ JavaScript บทช่วยสอนเกี่ยวกับ LiveProp, LiveAction, form และ deferred loading

Doctrine ORM: เชี่ยวชาญความสัมพันธ์ใน Symfony
คู่มือฉบับสมบูรณ์เกี่ยวกับความสัมพันธ์ Doctrine ORM ใน Symfony OneToMany, ManyToMany, กลยุทธ์การโหลดและการเพิ่มประสิทธิภาพพร้อมตัวอย่างจริง

คำถามสัมภาษณ์ Symfony: 25 อันดับแรกในปี 2026
25 คำถามสัมภาษณ์ Symfony ที่ถูกถามบ่อยที่สุด สถาปัตยกรรม, Doctrine ORM, บริการ, ความปลอดภัย, ฟอร์มและการทดสอบ พร้อมคำตอบละเอียดและตัวอย่างโค้ด