| name | symfony:functional-tests |
| description | Write functional tests for Symfony controllers and HTTP endpoints using WebTestCase, BrowserKit, and test clients |
Functional Tests for Symfony
Functional tests verify that your application works correctly from HTTP request to response.
Setup
composer require --dev symfony/test-pack
composer require --dev zenstruck/foundry
WebTestCase Basics
<?php
namespace App\Tests\Functional\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class HomeControllerTest extends WebTestCase
{
public function testHomePageLoads(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Welcome');
}
}
HTTP Client Methods
Request Methods
$client->request('GET', '/users');
$client->request('POST', '/api/users', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['email' => 'test@example.com']));
$client->request('PUT', '/api/users/1', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['name' => 'Updated']));
$client->request('DELETE', '/api/users/1');
$client->request('GET', '/api/users', [], [], [
'HTTP_AUTHORIZATION' => 'Bearer token123',
'HTTP_ACCEPT' => 'application/json',
]);
Response Assertions
$this->assertResponseIsSuccessful();
$this->assertResponseStatusCodeSame(201);
$this->assertResponseRedirects('/login');
$this->assertSelectorExists('form.login');
$this->assertSelectorTextContains('h1', 'Welcome');
$this->assertSelectorCount(5, '.user-card');
$response = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('id', $response);
$this->assertResponseHeaderSame('Content-Type', 'application/json');
Authentication in Tests
loginUser()
use App\Tests\Factory\UserFactory;
public function testProtectedEndpoint(): void
{
$client = static::createClient();
$user = UserFactory::createOne()->object();
$client->loginUser($user);
$client->request('GET', '/dashboard');
$this->assertResponseIsSuccessful();
}
With Different Roles
public function testAdminOnlyEndpoint(): void
{
$client = static::createClient();
$user = UserFactory::createOne(['roles' => ['ROLE_USER']])->object();
$client->loginUser($user);
$client->request('GET', '/admin');
$this->assertResponseStatusCodeSame(403);
$admin = UserFactory::createOne(['roles' => ['ROLE_ADMIN']])->object();
$client->loginUser($admin);
$client->request('GET', '/admin');
$this->assertResponseIsSuccessful();
}
Form Testing
public function testSubmitsContactForm(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/contact');
$form = $crawler->selectButton('Send')->form([
'contact[name]' => 'John Doe',
'contact[email]' => 'john@example.com',
'contact[message]' => 'Hello there!',
]);
$client->submit($form);
$this->assertResponseRedirects('/contact/thanks');
}
public function testFormValidation(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/contact');
$form = $crawler->selectButton('Send')->([
=> ,
]);
= ->();
->();
}
Testing with Crawler
public function testPageContent(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/users');
$this->assertCount(10, $crawler->filter('.user-card'));
$title = $crawler->filter('h1')->text();
$this->assertEquals('User List', $title);
$link = $crawler->filter('a.profile-link')->attr('href');
$this->assertEquals('/users/1', $link);
$link = $crawler->selectLink('View Profile')->link();
$client->click();
->();
}
API Testing
<?php
namespace App\Tests\Functional\Api;
use App\Tests\Factory\UserFactory;
use App\Tests\Factory\ProductFactory;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
class ProductApiTest extends WebTestCase
{
use Factories;
use ResetDatabase;
private $client;
protected function setUp(): void
{
$this->client = static::createClient();
}
public function testListsProducts():
{
::();
->client->(, );
->();
= (->client->()->(), );
->(, []);
}
{
= ::([ => []])->();
->client->();
->client->(, , [], [], [
=> ,
], ([
=> ,
=> ,
]));
->();
= (->client->()->(), );
->(, []);
}
{
= ::([ => []])->();
->client->();
->client->(, , [], [], [
=> ,
], ([
=> , // : name
]));
->();
}
}
Testing Email Sending
use Symfony\Bundle\FrameworkBundle\Test\MailerAssertionsTrait;
class RegistrationTest extends WebTestCase
{
use MailerAssertionsTrait;
public function testSendsWelcomeEmail(): void
{
$client = static::createClient();
$client->request('POST', '/register', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
'email' => 'new@example.com',
'password' => 'password123',
]));
$this->assertEmailCount(1);
$email = $this->getMailerMessage();
$this->assertEmailHtmlBodyContains($email, 'Welcome');
$this->assertEmailAddressContains($email, 'to', 'new@example.com');
}
}
Database Isolation
use Zenstruck\Foundry\Test\ResetDatabase;
class OrderTest extends WebTestCase
{
use ResetDatabase;
public function testCreatesOrder(): void
{
$user = UserFactory::createOne();
}
}
Best Practices
- One scenario per test: Each test should verify one behavior
- Use factories: Create test data with Foundry
- Reset database: Use
ResetDatabase for isolation
- Test both success and failure: Verify error handling
- Don't test implementation: Test HTTP interface, not internals