Rozdział 19: Testy funkcjonalne
Po testach jednostkowych (Rozdział 18) sprawdzamy blog tak, jak robi to przeglądarka — wysyłamy żądania
HTTP, klikamy, wypełniamy formularze i weryfikujemy odpowiedzi (również JSON z API). Służy do tego
WebTestCase.
Stan wejściowy: gotowy blog (Części I–IV) i testy jednostkowe (Rozdział 18). Tu dokładamy testy całych ścieżek żądanie → odpowiedź.
WebTestCase — Client, Crawler, Response
Test funkcjonalny działa na trzech obiektach: Client (symuluje przeglądarkę, wysyła żądania i trzyma sesję), Crawler (przeszukuje DOM selektorami CSS) i Response (sprawdzamy asercjami). Szkielet:
// tests/Functional/Controller/BlogControllerTest.php
namespace App\Tests\Functional\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
final class BlogControllerTest extends WebTestCase
{
public function testBlogIndexLoads(): void
{
$client = static::createClient();
$client->request('GET', '/blog');
$this->assertResponseIsSuccessful(); // status 2xx
$this->assertSelectorTextContains('h1', 'Blog');
}
}
Klienta tworzymy raz na test, przed pierwszym żądaniem. Dane testowe (artykuły) tworzymy w bazie testowej — DAMA cofa je po każdym teście (Rozdział 18).
Testowanie stron
Sprawdzamy status i treść odpowiedzi. Opublikowany artykuł jest widoczny, a szkic / nieistniejący slug dają 404:
public function testPublishedArticleIsVisible(): void
{
$client = static::createClient();
$article = $this->createArticle(BlogArticle::STATUS_PUBLISHED);
$client->request('GET', '/blog/' . $article->getSlug());
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', $article->getTitle());
}
public function testDraftArticleReturns404(): void
{
$client = static::createClient();
$article = $this->createArticle(BlogArticle::STATUS_DRAFT);
$client->request('GET', '/blog/' . $article->getSlug());
$this->assertResponseStatusCodeSame(404);
}
Najprzydatniejsze asercje: assertResponseIsSuccessful(), assertResponseStatusCodeSame(404),
assertResponseRedirects('/login'), assertSelectorTextContains('h1', '…'), assertSelectorExists('…').
Testowanie formularzy
submitForm() znajduje formularz po tekście przycisku, wypełnia pola i wysyła — wraz z tokenem CSRF.
Sprawdźmy dodanie komentarza (Rozdział 7): zalogowany użytkownik zostawia komentarz, który trafia do
moderacji (pending, więc niewidoczny publicznie):
public function testLoggedInUserCommentIsPending(): void
{
$client = static::createClient();
$article = $this->createArticle(BlogArticle::STATUS_PUBLISHED);
$client->loginUser($this->createUser());
$client->request('GET', '/blog/' . $article->getSlug());
$client->submitForm('Dodaj komentarz', [
'blog_comment[content]' => 'Świetny artykuł!',
]);
$this->assertResponseRedirects('/blog/' . $article->getSlug());
$em = static::getContainer()->get(EntityManagerInterface::class);
$comment = $em->getRepository(BlogComment::class)->findOneBy(['content' => 'Świetny artykuł!']);
$this->assertSame(BlogComment::STATUS_PENDING, $comment->getStatus());
}
W panelu admina formularz z błędem walidacji nie przekierowuje — Symfony renderuje go ponownie ze statusem 422:
public function testCreateArticleWithEmptyTitleShowsError(): void
{
$client = static::createClient();
$client->loginUser($this->createAdminUser());
$client->request('GET', '/admin/blog/create');
$client->submitForm('Utwórz artykuł', ['blog_article[title]' => '']);
$this->assertResponseStatusCodeSame(422);
}
Testowanie autoryzacji
Zamiast przechodzić przez formularz logowania, używamy loginUser(). Trzy przypadki pokrywają dostęp do
panelu: niezalogowany → redirect na /login, zła rola → 403, admin → 200:
public function testAdminRequiresLogin(): void
{
$client = static::createClient();
$client->request('GET', '/admin/blog');
$this->assertResponseRedirects('/login');
}
public function testAdminForbiddenForRegularUser(): void
{
$client = static::createClient();
$client->loginUser($this->createUser()); // zwykły ROLE_USER
$client->request('GET', '/admin/blog');
$this->assertResponseStatusCodeSame(403);
}
public function testAdminAccessibleForAdmin(): void
{
$client = static::createClient();
$client->loginUser($this->createAdminUser()); // ROLE_ADMIN
$client->request('GET', '/admin/blog');
$this->assertResponseIsSuccessful();
}
Testowanie API
Publiczne API zwraca JSON — odpowiedź odczytujemy metodą ->toArray():
public function testPublicApiReturnsJson(): void
{
$client = static::createClient();
$this->createArticle(BlogArticle::STATUS_PUBLISHED);
$client->request('GET', '/api/blog/articles');
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('Content-Type', 'application/json');
$data = $client->getResponse()->toArray();
$this->assertArrayHasKey('items', $data);
}
API admina (Rozdział 17) wymaga tokenu — bez niego 401, z tokenem w nagłówku Authorization: Bearer
200:
public function testAdminApiRequiresToken(): void
{
$client = static::createClient();
$client->request('GET', '/api/admin/blog/articles');
$this->assertResponseStatusCodeSame(401);
}
public function testAdminApiWithValidToken(): void
{
$client = static::createClient();
$this->createUser(roles: ['ROLE_ADMIN'], apiToken: 'test-token-123');
$client->request('GET', '/api/admin/blog/articles', server: [
'HTTP_AUTHORIZATION' => 'Bearer test-token-123',
]);
$this->assertResponseIsSuccessful();
}
Nagłówki przekazujemy w tablicy server z prefiksem HTTP_ (HTTP_AUTHORIZATION → nagłówek
Authorization).
Uruchamianie
make tests_unit # wszystkie testy
docker compose exec app php bin/phpunit tests/Functional # tylko funkcjonalne
docker compose exec app php bin/phpunit --filter testAdminRequiresLogin
Pamiętaj o bazie testowej (make db-test) — testy funkcjonalne realnie zapisują i czytają dane (DAMA cofa
je po każdym teście).
Podsumowanie
Blog ma pełną siatkę testów — od jednostki po żądanie HTTP:
- ✅
WebTestCasei trójka Client / Crawler / Response, - ✅ testowanie stron — asercje statusu i treści (opublikowany 200, szkic/nieistniejący 404),
- ✅ testowanie formularzy przez
submitForm()— komentarz (pending), walidacja artykułu (422), - ✅ testowanie autoryzacji przez
loginUser()— redirect / 403 / 200 dla panelu, - ✅ testowanie API — publiczne JSON (
toArray), API admina (401 bez tokenu, 200 zBearer), - ✅ działający stan:
make tests_unitprzechodzi na zielono, całe ścieżki bloga są przetestowane.
W Rozdziale 20 zajmiemy się deploymentem: wieloetapowy obraz Docker, overlay produkcyjny, CI/CD w GitHub Actions i bezpieczne migracje. A w Rozdziale 21 — ostatnim — domkniemy SEO: sitemap XML, hreflang/canonical (z Rozdziału 14) oraz meta tagi Open Graph.