MidПрактика5 min

Функциональные тесты

WebTestCase, Client, симуляция запросов, формы, редиректы

WebTestCase

Функциональные тесты проверяют поведение приложения через HTTP-запросы. WebTestCase предоставляет HTTP-клиент для симуляции запросов.

<?php

declare(strict_types=1);

namespace App\Tests\Functional;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class HomePageTest extends WebTestCase
{
    public function testHomepageReturnsSuccessfulResponse(): void
    {
        $client = static::createClient();

        $client->request('GET', '/');

        $this->assertResponseIsSuccessful();
        $this->assertResponseStatusCodeSame(200);
    }

    public function testHomepageContainsTitle(): void
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/');

        $this->assertSelectorTextContains('h1', 'Welcome');
        $this->assertSelectorExists('.hero-section');
    }
}

Client Object

Создание клиента

<?php

declare(strict_types=1);

namespace App\Tests\Functional;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class ApiTest extends WebTestCase
{
    public function testCreateClient(): void
    {
        // Basic client
        $client = static::createClient();

        // Client with server parameters
        $client = static::createClient([], [
            'HTTP_HOST' => 'example.com',
            'HTTP_ACCEPT' => 'application/json',
        ]);

        // With environment/debug options
        $client = static::createClient([
            'environment' => 'test',
            'debug' => true,
        ]);

        $client->request('GET', '/');
        $this->assertResponseIsSuccessful();
    }
}

HTTP-методы

<?php

declare(strict_types=1);

namespace App\Tests\Functional;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class ApiEndpointTest extends WebTestCase
{
    public function testGetRequest(): void
    {
        $client = static::createClient();

        $client->request('GET', '/api/users');

        $this->assertResponseIsSuccessful();
        $this->assertResponseHeaderSame('content-type', 'application/json');
    }

    public function testPostRequest(): void
    {
        $client = static::createClient();

        $client->request('POST', '/api/users', [], [], [
            'CONTENT_TYPE' => 'application/json',
        ], json_encode([
            'email' => '[email protected]',
            'name' => 'Test User',
        ]));

        $this->assertResponseStatusCodeSame(201);
    }

    public function testJsonRequest(): void
    {
        $client = static::createClient();

        // Shortcut for JSON requests
        $client->jsonRequest('POST', '/api/users', [
            'email' => '[email protected]',
            'name' => 'Test User',
        ]);

        $this->assertResponseStatusCodeSame(201);

        $response = $client->getResponse();
        $data = json_decode($response->getContent(), true);

        $this->assertArrayHasKey('id', $data);
        $this->assertSame('[email protected]', $data['email']);
    }

    public function testDeleteRequest(): void
    {
        $client = static::createClient();

        $client->request('DELETE', '/api/users/1');

        $this->assertResponseStatusCodeSame(204);
    }
}

Симуляция запросов

Параметры request()

<?php

declare(strict_types=1);

// Full signature:
$crawler = $client->request(
    method: 'POST',
    uri: '/submit',
    parameters: ['name' => 'John'],    // POST/GET parameters
    files: [],                          // Uploaded files
    server: [                           // Server variables
        'HTTP_ACCEPT_LANGUAGE' => 'en',
        'HTTP_AUTHORIZATION' => 'Bearer token123',
        'CONTENT_TYPE' => 'application/json',
    ],
    content: null,                      // Raw body content
    changeHistory: true,                // Add to browser history
);

Аутентификация в тестах

<?php

declare(strict_types=1);

namespace App\Tests\Functional;

use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class SecuredAreaTest extends WebTestCase
{
    public function testAdminDashboardRequiresAuth(): void
    {
        $client = static::createClient();

        $client->request('GET', '/admin');

        $this->assertResponseRedirects('/login');
    }

    public function testAdminDashboardAccessibleForAdmin(): void
    {
        $client = static::createClient();

        // Login as user (requires UserRepository fixture)
        $userRepository = static::getContainer()
            ->get('doctrine')
            ->getRepository(User::class);

        $user = $userRepository->findOneBy(['email' => '[email protected]']);

        $client->loginUser($user);

        $client->request('GET', '/admin');

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Dashboard');
    }

    public function testApiWithToken(): void
    {
        $client = static::createClient();

        $client->request('GET', '/api/profile', [], [], [
            'HTTP_AUTHORIZATION' => 'Bearer valid-token-here',
        ]);

        $this->assertResponseIsSuccessful();
    }
}

Работа с формами

<?php

declare(strict_types=1);

namespace App\Tests\Functional;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class RegistrationTest extends WebTestCase
{
    public function testUserCanRegister(): void
    {
        $client = static::createClient();

        // Load the page with the form
        $crawler = $client->request('GET', '/register');
        $this->assertResponseIsSuccessful();

        // Find the form and fill it
        $form = $crawler->selectButton('Register')->form([
            'registration_form[email]' => '[email protected]',
            'registration_form[password][first]' => 'SecurePass123!',
            'registration_form[password][second]' => 'SecurePass123!',
            'registration_form[agreeTerms]' => true,
        ]);

        // Submit the form
        $client->submit($form);

        // Assert redirect after successful registration
        $this->assertResponseRedirects('/login');

        // Follow the redirect
        $client->followRedirect();

        $this->assertSelectorTextContains('.flash-success', 'Registration successful');
    }

    public function testRegistrationValidation(): void
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/register');

        // Submit with invalid data
        $form = $crawler->selectButton('Register')->form([
            'registration_form[email]' => 'invalid-email',
            'registration_form[password][first]' => '123',
            'registration_form[password][second]' => '456',
        ]);

        $client->submit($form);

        // Should stay on same page with errors
        $this->assertResponseIsUnprocessable(); // 422
        $this->assertSelectorExists('.form-error-message');
    }
}

Работа с формой по шагам

<?php

declare(strict_types=1);

namespace App\Tests\Functional;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class ContactFormTest extends WebTestCase
{
    public function testContactFormSubmission(): void
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/contact');

        // Method 1: Select form by button text
        $form = $crawler->selectButton('Send Message')->form();

        // Set individual fields
        $form['contact[name]'] = 'John Doe';
        $form['contact[email]'] = '[email protected]';
        $form['contact[message]'] = 'Hello, this is a test message';

        // Set select field
        $form['contact[subject]']->select('support');

        // Set checkbox
        $form['contact[newsletter]']->tick();

        // Submit
        $client->submit($form);

        $this->assertResponseRedirects('/contact/thanks');
    }
}

Redirect Following

<?php

declare(strict_types=1);

namespace App\Tests\Functional;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class RedirectTest extends WebTestCase
{
    public function testLoginRedirectsToHomepage(): void
    {
        $client = static::createClient();

        // Submit login form...
        $client->request('POST', '/login', [
            'email' => '[email protected]',
            'password' => 'password',
        ]);

        // Assert redirect
        $this->assertResponseRedirects('/');

        // Follow the redirect
        $crawler = $client->followRedirect();

        // Now on the homepage
        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('nav', 'Logout');
    }

    public function testMaxRedirectFollowing(): void
    {
        $client = static::createClient();
        $client->setMaxRedirects(5);

        // Follow redirects automatically
        $client->followRedirects(true);

        $client->request('GET', '/old-page');

        // Already followed all redirects
        $this->assertResponseIsSuccessful();
    }
}

Assertions для ответов

<?php

declare(strict_types=1);

// Response assertions
$this->assertResponseIsSuccessful();               // 2xx
$this->assertResponseStatusCodeSame(200);           // Exact code
$this->assertResponseRedirects('/path');             // 3xx + location
$this->assertResponseRedirects('/path', 302);        // Exact redirect
$this->assertResponseHeaderSame('content-type', 'application/json');
$this->assertResponseHasHeader('X-Custom-Header');
$this->assertResponseNotHasHeader('X-Debug');
$this->assertResponseHasCookie('session');
$this->assertResponseFormatSame('json');

// Selector assertions (DOM)
$this->assertSelectorExists('h1');
$this->assertSelectorNotExists('.error');
$this->assertSelectorTextContains('h1', 'Welcome');
$this->assertSelectorTextSame('.title', 'Exact Title');
$this->assertSelectorCount(5, '.product-card');

// Email assertions
$this->assertEmailCount(1);
$this->assertQueuedEmailCount(1);

Доступ к контейнеру в тестах

<?php

declare(strict_types=1);

namespace App\Tests\Functional;

use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class UserTest extends WebTestCase
{
    public function testUserCreatedInDatabase(): void
    {
        $client = static::createClient();

        // Submit registration form...
        $client->request('POST', '/register', [/* ... */]);

        // Access the container to verify database state
        $container = static::getContainer();
        $userRepository = $container->get(UserRepository::class);

        $user = $userRepository->findOneBy(['email' => '[email protected]']);
        $this->assertNotNull($user);
        $this->assertSame('[email protected]', $user->getEmail());
    }
}

KernelTestCase для интеграционных тестов

<?php

declare(strict_types=1);

namespace App\Tests\Integration;

use App\Service\OrderProcessor;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

final class OrderProcessorTest extends KernelTestCase
{
    public function testProcessOrder(): void
    {
        self::bootKernel();

        $processor = static::getContainer()->get(OrderProcessor::class);

        $result = $processor->process(orderId: 42);

        $this->assertTrue($result->isSuccessful());
    }
}

Итоги

  • WebTestCase -- функциональные тесты с HTTP-клиентом
  • createClient() -- создание тестового клиента
  • $client->request() -- симуляция HTTP-запроса
  • $client->loginUser() -- аутентификация в тестах
  • selectButton()->form() -- заполнение и отправка форм
  • followRedirect() -- следование за редиректами
  • assertResponseIsSuccessful(), assertSelectorTextContains() и др.
  • KernelTestCase -- тесты с контейнером (без HTTP)