
Summary:
Learn to test Symfony 7.2 APIs with PHPUnit, covering JWT auth, mocking, validation, and secure test setups.
June 24, 2025
Building robust APIs in Symfony 7.2 requires thorough testing to ensure reliability and security. In this guide, we’ll use PHPUnit to test the API endpoints from JWT authentication setup -/api/register, /api/login_check, /api/users/{id}, /api/users/me, and /api/profile. You’ll learn to set up PHPUnit, write unit and functional tests, mock dependencies, and verify authentication and validation.
Getting Started with Unit Testing and PHPUnit
Unit testing focuses on small and independent sections of code, such as a single method, to verify that it performs as intended. For example, we can test a password validation function to ensure that it blocks invalid passwords and requires a minimum of 8 characters – a small win that saves issues later. Functional testing examines the full application’s behaviour, like sending a request to ~/api/register and confirming that the response is correct.
PHPUnit is the most reliable PHP testing framework, simplifying the process of creating and running tests. It pairs well with Symfony, helping to automate checks, catch the issues early, and give confidence when modifying code. For APIs like JWT authentication or user registration logic, testing is essential to guarantee stability across multiple scenarios; a single oversight could open a security gap or break the functionality.
This guide is based on the Symfony 7.2 project, where initial test failures, like a persistent 404 on ~/api/login_check, taught me key lessons. It is structured to help you set up PHPUnit and build both unit and functional tests.
Technical Tips for Symfony Unit Testing
- Use KernelTestCase: We can use the KernelTestCase class to boot the Symfony kernel and access services like the EntityManager, which lets us mock database operations for clean, isolated unit tests.
- Mock Dependencies in Controller: When testing something like AuthController, use PHPUnit’s createMock or Mockery to simulate dependencies.
- Set Up a .env.test File: Set up a separate .env.test file; it’s invaluable. It configures a test database so we can run Doctrine migrations safely without risking production data.
- Validate Entities with ValidatorInterface: To catch edge cases, we can include the ValidatorInterface in tests, mocking errors to check for issues like invalid emails or missing fields-saved me during a recent debug session.
- Mock HTTP Requests with Request Objects: We’ve started using Request objects to mimic HTTP calls, which helps me test request processing and response building without needing a live server setup.
Prerequisites
- Symfony 7.2 project with JWT setup (using LexikJWTAuthenticationBundle)
- PHP 8.4
- MySQL 8.0
- Composer
- PHPUnit
Step 1: Set Up PHPUnit
Ensure PHPUnit is installed and configured in your Symfony project.
Install PHPUnit via Composer
composer require –dev phpunit/phpunit “^10.0” symfony/test-pack
Configure PHPUnit
Copy the default configuration and update phpunit.xml to include your test environment settings and test suites, pointing to your .env.test file:
<php> <env name="APP_ENV" value="test"/> <env name="DATABASE_URL" value="mysql://user:password@localhost:3306/symfony7_api_demo_test_test?serverVersion=8.0&charset=utf8mb4"/> </php> <testsuites> <testsuite name="Unit"> <directory>tests/Unit</directory> </testsuite> <testsuite name="Functional"> <directory>tests/Functional</directory> </testsuite> </testsuites>
Note: Adjust the DATABASE_URL to match your test database. The section organizes tests into Unit and Functional suites for targeted execution.
Create a Test Database and Load the Schema
Run these commands manually to reset the database:
php bin/console doctrine:database:drop --env=test --force php bin/console doctrine:database:create --env=test php bin/console doctrine:migrations:migrate --env=test php bin/console doctrine:fixtures:load --env=test --no-interaction
Step 2: Write Unit Tests for Authentication Logic
Let’s test the AuthController’s register method to ensure validation works.
Original Controller Code
Here’s the AuthController, which is going to be tested,
#create src/Controller/AuthController.php <?php // src/Controller/AuthController.php namespace App\Controller; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; class AuthController { private $entityManager; private $passwordHasher; private $jwtManager; private $validator; public function __construct( EntityManagerInterface $entityManager, UserPasswordHasherInterface $passwordHasher, JWTTokenManagerInterface $jwtManager, ValidatorInterface $validator ) { $this->entityManager = $entityManager; $this->passwordHasher = $passwordHasher; $this->jwtManager = $jwtManager; $this->validator = $validator; } public function register(Request $request): JsonResponse { $data = json_decode($request->getContent(), true); $user = new User(); $user->setEmail($data['email'] ?? ''); $user->setName($data['name'] ?? ''); $errors = $this->validator->validate($user); if (count($errors) > 0) { return new JsonResponse(['errors' => (string) $errors], 400); } if (strlen($data['password'] ?? '') < 8) { return new JsonResponse(['error' => 'Password must be at least 8 characters'], 400); } $hashedPassword = $this->passwordHasher->hashPassword($user, $data['password']); $user->setPassword($hashedPassword); $this->entityManager->persist($user); $this->entityManager->flush(); $token = $this->jwtManager->create($user); return new JsonResponse([ 'message' => 'User registered successfully', 'user' => ['id' => $user->getId(), 'email' => $user->getEmail(), 'name' => $user->getName()], 'token' => $token ], 201); } }
Note: This controller handles user registration, validates input, hashes the password, persists the user, and generates a JWT token.
Create a Test File for AuthController
#create tests/Unit/AuthControllerTest.php: <?php // tests/Unit/AuthControllerTest.php namespace App\Tests\Unit; use App\Controller\AuthController; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Validator\ValidatorInterface; class AuthControllerTest extends TestCase { private $entityManager; private $passwordHasher; private $jwtManager; private $validator; private $controller; protected function setUp(): void { $this->entityManager = $this->createMock(EntityManagerInterface::class); $this->passwordHasher = $this->createMock(UserPasswordHasherInterface::class); $this->jwtManager = $this->createMock(JWTTokenManagerInterface::class); $this->validator = $this->createMock(ValidatorInterface::class); $this->controller = new AuthController( $this->entityManager, $this->passwordHasher, $this->jwtManager, $this->validator ); } public function testRegisterWithInvalidPassword(): void { $violations = new ConstraintViolationList(); $violations[] = new ConstraintViolation('Email is required', '', [], null, 'email', null); $this->validator->method('validate')->willReturn($violations); $request = new Request([], [], [], [], [], [], json_encode(['email' => '', 'name' => 'John', 'password' => 'short'])); $response = $this->controller->register($request); $this->assertInstanceOf(JsonResponse::class, $response); $this->assertEquals(400, $response->getStatusCode()); $data = json_decode($response->getContent(), true); $this->assertArrayHasKey('errors', $data); $this->assertArrayHasKey('email', $data['errors']); } public function testRegisterWithValidData(): void { $user = $this->createMock(User::class); $user->method('getId')->willReturn(1); $user->method('getEmail')->willReturn('test@example.com'); $user->method('getName')->willReturn('John'); $this->validator->method('validate')->willReturn(new ConstraintViolationList()); $this->passwordHasher->method('hashPassword')->willReturn('hashedpassword'); $this->entityManager->method('persist')->willReturnCallback(function ($entity) use ($user) { if ($entity === $user) return; }); $this->entityManager->method('flush'); $request = new Request([], [], [], [], [], [], json_encode(['email' => 'test@example.com', 'name' => 'John', 'password' => 'test12345'])); $response = $this->controller->register($request); $this->assertInstanceOf(JsonResponse::class, $response); $this->assertEquals(201, $response->getStatusCode()); $data = json_decode($response->getContent(), true); $this->assertArrayHasKey('message', $data); $this->assertArrayHasKey('user', $data); $this->assertEquals('test@example.com', $data['user']['email']); }
Step 3: Write Functional Tests for API Endpoints
Let’s test the API endpoints using Symfony’s WebTestCase to simulate HTTP requests.
Create a Test File
#create tests/Functional/ApiFunctionalTest.php: <?php // tests/Functional/ApiFunctionalTest.php namespace App\Tests\Functional; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class ApiFunctionalTest extends WebTestCase { private KernelBrowser $client; protected function setUp(): void { $this->client = static::createClient([], ['HTTP_HOST' => '127.0.0.1:8000']); } public function testRegisterEndpoint(): void { $email = 'newtest' . time() . '@example.com'; $this->client->request('POST', '/api/register', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode([ 'email' => $email, 'name' => 'New User', 'password' => 'test12345' ])); $this->assertEquals(201, $this->client->getResponse()->getStatusCode()); } public function testLoginCheckEndpoint(): void { $this->client->request('POST', '/api/login_check', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode([ 'email' => 'jane@example.com', 'password' => 'password123' ])); $response = $this->client->getResponse(); $content = $response->getContent(); echo "Login Response: $content\n"; $this->assertEquals(200, $response->getStatusCode(), 'Login failed: ' . $content); $this->assertJson($content); $data = json_decode($content, true); $this->assertArrayHasKey('token', $data, 'No token returned: ' . json_encode($data)); } public function testProtectedEndpointWithoutToken(): void { $this->client->request('GET', '/api/users/1'); $this->assertEquals(401, $this->client->getResponse()->getStatusCode()); } public function testProtectedEndpointWithToken(): void { $this->client->request('POST', '/api/login_check', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode([ 'email' => 'jane@example.com', 'password' => 'password123' ])); $data = json_decode($this->client->getResponse()->getContent(), true); $token = $data['token'] ?? null; if ($token) { $this->client->request('GET', '/api/users/1', [], [], ['HTTP_Authorization' => 'Bearer ' . $token]); $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); $this->assertJson($this->client->getResponse()->getContent()); } else { $this->fail('Failed to obtain JWT token: ' . json_encode($data)); } }
Step 4: Mock JWT Authentication
To test protected endpoints, we initially struggled with token generation but resolved it by relying on the bundle. The test now uses the real authentication flow.
Update ApiFunctionalTest.php
The testProtectedEndpointWithToken method above already incorporates this, using the token from /api/login_check. No additional mocking is needed since the bundle handles it.
Step 5: Run Unit Tests
Run your unit tests to verify the application’s logic. This step ensures your code works as expected.
Run Command PHPUnit
vendor/bin/phpunit –testsuite Unit
Sample Output:
Dropped database `symfony7_api_demo_test_test` for connection named default
Created database `symfony7_api_demo_test_test` for connection named default
Updating database schema…
1 query was executed
[OK] Database schema updated successfully!
> purging database > loading App\DataFixtures\AppFixtures PHPUnit 10.5.46 by Sebastian Bergmann and contributors. Runtime: PHP 8.4.7 Configuration: /home/brainstream/workspace/symfony7apidemo/phpunit.xml .. 2 / 2 (100%) Time: 00:00.026, Memory: 24.00 MB OK (2 tests, 9 assertions)
Note: This output shows two unit tests passing, confirming the AuthControllerTest works. Run this after setting up the database to see your results. Use –testsuite Functional to run functional tests separately.
Why Test Your APIs?
- Reliability: Catch bugs early with automated tests, as we did with the container and routing issues.
- Security: Verify authentication and validation logic, critical for JWT setups.
- Maintainability: Ensure changes don’t break existing functionality, a lesson from our iterative fixes.
Conclusion
You’ve set up PHPUnit to test your Symfony 7.2 API, covering unit and functional tests for JWT-authenticated endpoints. Our journey from test failures (container errors, 404 error) to success highlights the power of debugging and configuration tweaks. With this foundation, your API is more robust and ready for production.
Explore our complete Symfony development services to build and test high-performance APIs with confidence.
Related Blog
Symfony
Securing Symfony APIs with JWT Authentication
This guide shows you how to secure a Symfony 7.2 REST API with JWT authentication using lexik/jwt-authentication-bundle. You’ll set up user registration with validation, a rate-limited login endpoint, and protect routes like /api/users/{id} and /api/profile.
Symfony
Migrating from FOSUserBundle to Symfony Security Bundle (with ResetPasswordBundle & VerifyEmailBundle)
If you're still using FOSUserBundle in your Symfony project, it's time to upgrade. FOSUserBundle was once the standard solution for user authentication and management in Symfony, but it is now deprecated and unsupported in Symfony 6 and beyond. The good...
Symfony
Build Symfony REST API with OpenAPI and Swagger UI Integration
This guide shows you how to build your first REST API endpoint (/api/users/{id}) in Symfony 7.2, document it with OpenAPI YAML, and integrate Swagger UI for interactive testing. Using nelmio/api-doc-bundle and a hybrid approach, we’ll keep code clean, generate openapi.yaml,...

Keep up-to-date with our newsletter.
Sign up for our newsletter to receive weekly updates and news directly to your inbox.