19

Rozdział 19 z 21

Testy funkcjonalne

Opublikowany

Testujemy blog jak przeglądarka przez WebTestCase — oparte na realnym BlogControllerTest projektu. Sprawdzimy listę i strony artykułów, filtry kategorii/tagów, formularze (submitForm), autoryzację (loginUser) oraz endpointy API.

Czego się nauczysz

  • WebTestCase — Client, Crawler, Response
  • Testowanie stron: status i treść
  • Testowanie formularzy (submitForm)
  • Testowanie autoryzacji (loginUser, 403/redirect)
  • Testowanie API (jsonRequest, toArray)

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:

  • ✅ WebTestCase i 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 z Bearer),
  • ✅ działający stan: make tests_unit przechodzi 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.

Narzędzia / paczki

WebTestCase symfony/browser-kit symfony/dom-crawler

Spis treści