Functional Tests With WebTestCase
Functional Tests With WebTestCase
~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Unit tests from chapter 42 check ISOLATED logic – now let's test the INTERPLAY: a REAL HTTP request to /login, running through routing, security, AND the controller.
The WebTestCase base class
<?php
declare(strict_types=1);
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class SecurityControllerTest extends WebTestCase
{
public function testLoginPageLoads(): void
{
$client = static::createClient();
$client->request('GET', '/login');
self::assertResponseIsSuccessful();
self::assertSelectorExists('form');
}
}createClient() creates a simulated HTTP client that tests AGAINST an ACTUALLY booted Symfony application (in the test environment from chapter 4) – request() fires a REAL request that runs through routing (chapter 7), security (block 5), AND the controller.
Important assertions for functional tests
assertResponseIsSuccessful()– a 2xx status code.assertResponseStatusCodeSame(404)– a SPECIFIC status code.assertResponseRedirects('/login')– redirects to a specific route.assertSelectorExists('form')– a CSS selector finds an element in the rendered HTML.assertSelectorTextContains('h1', 'Log In')– an element contains specific text.
Filling out and submitting a form
public function testRegistrationWorks(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/register');
$client->submitForm('Register', [
'registration_form[name]' => 'Test User',
'registration_form[email]' => 'test@example.com',
'registration_form[plainPassword]' => 'secure-password-123',
]);
self::assertResponseRedirects('/login');
}submitForm('Register', [...]) finds the button with the text "Register" (translated from the form's submit button), fills in the given field names (in Symfony Forms format from chapter 15: form_name[field_name]), and clicks it – SIMULATING real user behavior, INCLUDING the CSRF token built in chapter 18, which is AUTOMATICALLY included in the form and submitted along with it.
Simulating a logged-in user
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
public function testProtectedPageRequiresLogin(): void
{
$client = static::createClient();
// Without login: redirected to the login page
$client->request('GET', '/projects');
self::assertResponseRedirects('/login');
// With a simulated login: successful access
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
$user = $entityManager->getRepository(User::class)->findOneBy(['email' => 'anna@example.com']);
$client->loginUser($user);
$client->request('GET', '/projects');
self::assertResponseIsSuccessful();
}loginUser() bypasses the ACTUAL login form process and authenticates the test client DIRECTLY – faster than filling out the login form every time, when it's NOT the login process itself being tested, but some OTHER, protected page.
static::getContainer(): accessing services in tests
getContainer() gives access to the SAME service container from block 6 – handy in tests, e.g. to directly create test data via EntityManagerInterface, instead of clicking through the entire registration workflow.
Tipp: Rule of thumb for block 7: unit tests (chapter 42) for ISOLATED business logic, functional tests (this chapter) for CRITICAL user workflows (login, registration, access control) – a HEALTHY project typically has CONSIDERABLY more unit tests than functional tests, since the latter are slower.