Marketplace
symfony:functional-tests
Write functional tests for Symfony controllers and HTTP endpoints using WebTestCase, BrowserKit, and test clients
$ Instalar
git clone https://github.com/MakFly/superpowers-symfony /tmp/superpowers-symfony && cp -r /tmp/superpowers-symfony/skills/functional-tests ~/.claude/skills/superpowers-symfony// tip: Run this command in your terminal to install the skill
SKILL.md
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
// tests/Functional/Controller/HomeControllerTest.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
// GET request
$client->request('GET', '/users');
// POST with JSON
$client->request('POST', '/api/users', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['email' => 'test@example.com']));
// PUT
$client->request('PUT', '/api/users/1', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode(['name' => 'Updated']));
// DELETE
$client->request('DELETE', '/api/users/1');
// With headers
$client->request('GET', '/api/users', [], [], [
'HTTP_AUTHORIZATION' => 'Bearer token123',
'HTTP_ACCEPT' => 'application/json',
]);
Response Assertions
// Status codes
$this->assertResponseIsSuccessful(); // 2xx
$this->assertResponseStatusCodeSame(201);
$this->assertResponseRedirects('/login');
// Content
$this->assertSelectorExists('form.login');
$this->assertSelectorTextContains('h1', 'Welcome');
$this->assertSelectorCount(5, '.user-card');
// JSON
$response = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('id', $response);
// Headers
$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();
// Regular user - should fail
$user = UserFactory::createOne(['roles' => ['ROLE_USER']])->object();
$client->loginUser($user);
$client->request('GET', '/admin');
$this->assertResponseStatusCodeSame(403);
// Admin user - should succeed
$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');
// Select form and fill fields
$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')->form([
'contact[email]' => 'invalid-email',
]);
$crawler = $client->submit($form);
$this->assertSelectorExists('.invalid-feedback');
}
Testing with Crawler
public function testPageContent(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/users');
// Count elements
$this->assertCount(10, $crawler->filter('.user-card'));
// Get text
$title = $crawler->filter('h1')->text();
$this->assertEquals('User List', $title);
// Get attribute
$link = $crawler->filter('a.profile-link')->attr('href');
$this->assertEquals('/users/1', $link);
// Follow link
$link = $crawler->selectLink('View Profile')->link();
$client->click($link);
$this->assertRouteSame('user_show');
}
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(): void
{
ProductFactory::createMany(5);
$this->client->request('GET', '/api/products');
$this->assertResponseIsSuccessful();
$data = json_decode($this->client->getResponse()->getContent(), true);
$this->assertCount(5, $data['hydra:member']);
}
public function testCreatesProduct(): void
{
$admin = UserFactory::createOne(['roles' => ['ROLE_ADMIN']])->object();
$this->client->loginUser($admin);
$this->client->request('POST', '/api/products', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
'name' => 'New Product',
'price' => 1999,
]));
$this->assertResponseStatusCodeSame(201);
$data = json_decode($this->client->getResponse()->getContent(), true);
$this->assertEquals('New Product', $data['name']);
}
public function testValidatesProduct(): void
{
$admin = UserFactory::createOne(['roles' => ['ROLE_ADMIN']])->object();
$this->client->loginUser($admin);
$this->client->request('POST', '/api/products', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
'name' => '', // Invalid: empty name
]));
$this->assertResponseStatusCodeSame(422);
}
}
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; // Resets database before each test
public function testCreatesOrder(): void
{
// Database is clean here
$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
ResetDatabasefor isolation - Test both success and failure: Verify error handling
- Don't test implementation: Test HTTP interface, not internals
Repository

MakFly
Author
MakFly/superpowers-symfony/skills/functional-tests
26
Stars
4
Forks
Updated3d ago
Added5d ago