Тестування

Дата оновлення перекладу 2024-06-10

Тестування

Як тільки ви пишете новий рядок коду, ви також потенційно додаєте нові помилки. Для того щоб створити надійніші додатки, ви маєте тестувати їх, використовуючи як функціональні так і модульні тести.

Тестовий фреймворк PHPUnit

У Symfony інтегрована незалежна бібліотека під назвою PHPUnit, яка надає вам багатий тестовий фреймворк. Ця стаття не охоплює всіх нюансів PHPUnit, але й нього є своя детальна документація.

До створення вашого першого тесту, встановіть phpunit/phpunit та symfony/test-pack, який встановлює деякі інші пакети, що надають корисні тестові утиліти Symfony:

1
$ composer require --dev phpunit/phpunit symfony/test-pack

Після установки бібліотеки, спробуйте запустити PHPUnit:

1
$ php ./vendor/bin/phpunit

Ці команди автоматично запускають тести вашого додатку. Кожний тест - це PHP-клас, який закінчується на "Test" (наприклад, BlogControllerTest), і який живе у каталозі tests/ вашого додатку.

PHPUnit конфігурується файлом phpunit.xml.dist у корені вашого додатку. Конфігурації за замовчуванням наданої Symfony Flex буде достатньо у більшості випадків. Прочитайте документацію PHPUnit, щоб дізнатися всі можливі опції конфігурації (наприклад, підключення охоплення коду або розділення тестів на багато "наборів тестів").

Note

Symfony Flex автоматично створює phpunit.xml.dist і tests/bootstrap.php. Якщо цих файлів немає, ви можете спробувати запустити рецепт, використовуючи composer recipes:install phpunit/phpunit --force -v.

Типи тестів

Існує багато типів автоматизованих тестів і точні визначення часто розрізняються від проекту до проекту. В Symfony використовуються наступні визначення. Якщо ви вивчали щось інше, це не обов'язково неправильно, просто відрізняється від документації, яку використовує Symfony.

Модульні тести
Ці тести гарантують, що окремі модулі початкового коду (наприклад, один клас) поводять себе так, як мають.
Тести інтеграції
Ці тести тестують комбінацію класів і часто взаємодіють з серівс-контейнером Symfony. Ці тести не охоплюють повністю робочий застосунок, такі тести називаються тестами додатку.
Тести додатку
Тести додатку тестують поведінку повного додатку. Вони робять HTTP-запити (і реальні, і фіктивні) та тестують, щоб відповідь була очікуваною.

Модульні тести

Модульний тест гарантує, що окреми модулі початкового коду (наприклад, один клас або якийсь конкретний метод у класі) відповідають своєму дизайну та поводять себе так, як мають. Написання модульних тестів у додатку Symfony не відрізняється від написання звичайних модульних тестів PHPUnit. Ви можете дізнатися про це в документації PHPUnit: Написання тестів для PHPUnit.

За згодою, каталог tests/ має бути реплікою каталогу вашого додатку для модульних тестів. Тому, якщо ви тестуєте клас у каталозі src/Form/, помістить ваш тест у каталог tests/Form/. Автозавантаження вмикається автоматично через файл vendor/autoload.php (як сконфігуровано за замовчуванням файлом phpunit.xml.dist).

Ви можете запускати тести, використовуючи команду ./vendor/bin/phpunit:

1
2
3
4
5
6
7
8
# запустити всі тести додатку
$ php ./vendor/bin/phpunit

# запустити всі тести в каталозі Form/
$ php ./vendor/bin/phpunit tests/Form

# запустити всі тести для класу UserType
$ php ./vendor/bin/phpunit tests/Form/UserTypeTest.php

Tip

У великих наборах тестів має сенс створювати підкаталоги для всіх типів тестів (наприклад, tests/Unit/ і test/Functional/).

Тести інтеграції

Тест інтеграції тестуватиме більшу частину вашого додатку у порівнянні з модульним тестом (наприклад, комбінацію сервісів). Тести інтеграції можуть хотіти використовувати ядро Symfony для отримання сервісу з контейнера впровадження залежності.

Symfony надає клас KernelTestCase, щоб допомогти вам у створенні та запуску ядра у ваших тестах, використовуючи bootKernel():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tests/Service/NewsletterGeneratorTest.php
namespace App\Tests\Service;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class NewsletterGeneratorTest extends KernelTestCase
{
    public function testSomething(): void
    {
        self::bootKernel();

        // ...
    }
}

KernelTestCase також переконується, що ваше ядро перезапускається для кожного тесту. Це гарантує, що кожний тест виконоується незалежно від інших.

Щоб запустити тести вашого додатку, класу KernelTestCase необхідно знайти ядро додатку для ініціалізації. Клас ядра зазвичай визначається у змінній середовища KERNEL_CLASS (включена в файл за замовчуванням .env.test, наданий Symfony Flex):

1
2
# .env.test
KERNEL_CLASS=App\Kernel

Note

Якщо ваш приклад використання складніший, ви також можете перевизначити методи getKernelClass() або createKernel() вашого функціонального тесту, який функционального теста, який перевершує змінну середовища KERNEL_CLASS.

Налаштуйте середовище вашого тесту

Тести створюють ядро, яке працює у середовищі test. Це дозволяє мати спеціальні налаштування для ваших тестів всередині config/packages/test/.

Якщо у вас установлений Symfony Flex, деякі пакети вже встановили якусь корисну конфігурацію тестів. Наприклад, за замовчуванням, пакет Twig сконфігурований так, щоб бути особливо строгим у виявленні помилок до розгортання вашого коду у виробництво:

1
2
3
# config/packages/test/twig.yaml
twig:
    strict_variables: true

Ви також можете використат повністю інше середовище, або перевизначити режим налагодження за замовчуванням (true), передавший кожний як опції методу bootKernel():

1
2
3
4
self::bootKernel([
    'environment' => 'my_test_env',
    'debug'       => false,
]);

Tip

Рекомендується запускати ваш тест з налаштуванням debug як false на вашому сервері CI, так як це значно покращує продуктивність тесту. Якщо ваші тести не працюють у чистому середовищі кожний раз, вам потрібно вручну очистити його, використовуючи екземпляр цього коду в tests/bootstrap.php:

1
2
3
4
// ...

// гарантувати свіжий кеш при відключенні режиму налагодження
(new \Symfony\Component\Filesystem\Filesystem())->remove(__DIR__.'/../var/cache/test');

Налаштування змінних середовища

Якщо вам потрбіно налаштувати деякі змінні середовища для ваших тестів (наприклад, DATABASE_URL, використовуваний Doctrine), ви можете зробити це перевизначивши все, що вам необхідно, у файлі .env.test:

1
2
3
4
# .env.test

# ...
DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name_test?serverVersion=5.7"

У середовищі тестування, ці файли середовища читаються (якщо змінні у них дубльовані, файли нижче за списком перевизначають попередні об'єкти):

  1. .env: який містить змінні середовища із значенням додатку за замовчуванням;
  2. .env.test: перевизначення/установка конкретних значень або змінних тесту;
  3. .env.test.local: перевизначення налаштувань конкретно цієї машини.

Caution

Файл .env.local не використовується у середовищі тестування, щоб гарантувати, що кожний тест встановлено настільки узгоджено, наскільки це можливо.

Отримання сервісів у тесті

У ваших тестах інтеграції вам часто може бути потрібно отримати сервіс з сервіс-контейнера, щоб викликати якийсь метод. Після запуску ядра, контейнер зберігається в self::$container:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// tests/Service/NewsletterGeneratorTest.php
namespace App\Tests\Service;

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

class NewsletterGeneratorTest extends KernelTestCase
{
    public function testSomething(): void
    {
        // (1) запустити ядро Symfony
        self::bootKernel();

        // (2) використати self::$container, щоб отримати доступ до сервіс-контейнеру
        $container = self::$container;

        // (3) запустити якісь сервіси та протестувати результат
        $newsletterGenerator = $container->get(NewsletterGenerator::class);
        $newsletter = $newsletterGenerator->generateMonthlyNews(...);

        $this->assertEquals(..., $newsletter->getContent());
    }
}

Контейнер у self::$container насправді - спеціальний контейнер тестів. Він надає вам доступ як до публічних сервісів, так і до невидалених приватних сервісів .

Note

Якщо вам потрібно протестувати приватні сервіси, які були видалені (ті, які не використовуються ніякими іншими сервісами), вам потрібно оголосити ці приватні сервіси публічними у файлі config/services_test.yaml.

Імітація залежностей

Іноді може бути корисно імітувати залежність тестованого сервісу. З прикладу у попередньому розділі, давайте припутимо, що NewsletterGenerator має залежність від приватного псевдоніма NewsRepositoryInterface, який вказує на приватний сервіс NewsRepository, і ви хочете використати зімітований NewsRepositoryInterface замість реального:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...
use App\Contracts\Repository\NewsRepositoryInterface;

class NewsletterGeneratorTest extends KernelTestCase
{
    public function testSomething(): void
    {
        // ... таке ж початкове завантаження, як у розділі вище

        $newsRepository = $this->createMock(NewsRepositoryInterface::class);
        $newsRepository->expects(self::once())
            ->method('findNewsFromLastMonth')
            ->willReturn([
                new News('some news'),
                new News('some other news'),
            ])
        ;

        // наступний рядок не працюватиме, якщо псевдонім не публічний
        $container->set(NewsRepositoryInterface::class, $newsRepository);

        // буде проваджено зімітоване сховище
        $newsletterGenerator = $container->get(NewsletterGenerator::class);

        // ...
    }
}

Ніякої додаткої конфігурації не потрібно, оскільки тестовий сервіс-контейнер є спеціальним, і дозволяє вам взаємодіяти з приватними сервісами та псевдонімами.

Конфігурація бази даних для тестів

Тести, які взаємодіють з базою даних, мають використовувати свою власну окрему базу даних, щоб не порушувати нічого в базі даних в інших середовищах конфігурації .

Щоб зробити так, відредагуйте або створіть файл .env.test.local у кореневому каталозі вашого проекту, та визначте нове значення для змінної середовища DATABASE_URL:

1
2
# .env.test.local
DATABASE_URL="mysql://USERNAME:PASSWORD@127.0.0.1:3306/DB_NAME?serverVersion=5.7"

Передбачається, що кожний розробник/машина використовує різні бази даних для тестів. Якщо налаштування тесту однакові на всіх машинах, замість цього використайте файл .env.test та відправте його у спільне сховище. Дійзнайтеся більше про використання декількох файлів .env у додатках Symfony .

Після цього ви можете створити базу даних тестів і всі таблиці, використовуючи:

1
2
3
4
5
# створіть базу даних тестів
$ php bin/console --env=test doctrine:database:create

# створіть таблиці/стовпці у базі даних тестів
$ php bin/console --env=test doctrine:schema:create

Tip

Ви можете запускати ці команди для створення бази даних під час процесу тестового завантаження.

Tip

Розповсюдженою практикою є додавання суфікса _test до початкових назв баз даних у тестах. Якщо ім'я бази даних у виробництві - project_acme, то ім'я бази даних тестування може бути project_acme_test.

Автоматичне скидання бази даних перед кожним тестом

Тести мають бути незалежними один від одного, щоб уникнути побічних ефектів. Наприклад, якщо якийсь тест змінює базу даних (додаючи чи видаляючи сутність), він може змінити результати інших тестів.

DAMADoctrineTestBundle використовує транзакції Doctrine, щоб дозволити кожному тесту взаємодіяти з незмінною базою даних. Встановіть його, використовуючи:

1
$ composer require --dev dama/doctrine-test-bundle

Тепер включіть його як розширення PHPUnit:

1
2
3
4
5
6
7
8
<!-- phpunit.xml.dist -->
<phpunit>
    <!-- ... -->

    <extensions>
        <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
    </extensions>
</phpunit>

Ось і все! Цей пакет використовує розумний фокус: він починає транзакцію бази даних перед кожним тестом і автоматично скидає її після закінчення тесту, щоб відмінити всі зміни. Прочитайте більше у документації DAMADoctrineTestBundle.

Завантаження фікстур фіктивних даних

Замість використання реальних даних з бази даних виробництва, у базі даних тестів часто використовуються хибні або фіктивні дані. Це зазвичай називається "фікстурними даними" і Doctrine надає бібліотеку для їх завантаження та створення. Встановіть її:

1
$ composer require --dev doctrine/doctrine-fixtures-bundle

Потім використайте команду make:fixtures пакета SymfonyMakerBundle, щоб згенерувати пустий клас фікстури:

1
2
3
4
$ php bin/console make:fixtures

Ім'я класу фікстур для створення (наприклад, AppFixtures):
> ProductFixture

Потім ви змінюєте цей клас, щоб завантажувати нові сутності в базу даних. Наприклад, щоб завантажити об'єкти Product у Doctrine, використайте:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/DataFixtures/ProductFixture.php
namespace App\DataFixtures;

use App\Entity\Product;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class ProductFixture extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $product = new Product();
        $product->setName('Priceless widget');
        $product->setPrice(14.50);
        $product->setDescription('Ok, I guess it *does* have a price');
        $manager->persist($product);

        // додати більше продуктів

        $manager->flush();
    }
}

Очистіть базу даних та перезавантажте всі класи фікстур за допомогою:

1
$ php bin/console doctrine:fixtures:load

Щоб дізнатися більше, прочитайте документацію DoctrineFixturesBundle.

Тести додатку

Тести додатку перевіряють інтеграцію всіх шарів додатку (від маршрутизації до переглядів). Вони не відрізняються від модульних тестів або тестів інтеграції в контексті PHPUnit, але вони мають дуже конкретний робочий процес:

  1. Зробити запит ;
  2. Взаємодіяти зі сторінкою (наприклад, натиснути на посилання або відправити форму);
  3. Протестувати відповідь ;
  4. Повторити.

Note

Інструменти, використовувані у цьому розділі, можуть бути встановлені через symfony/test-pack, використайте composer require symfony/test-pack, якщо ви ще цього не зробили.

Напишіть свій перший тест додатку

Тести додатку - це PHP-файли, які зазвичай живуть у каталозі вашого проекту tests/Controller/. Вони часто розширюють WebTestCase. Цей клас додає спеціальну логіку над KernelTestCase. Ви можете прочитати більше про це вище, у розділі про тести інтеграції .

Якщо ви хочете простестувати сторінки з якими працює ваш клас PostController, почніть зі створення нового PostControllerTest, використовуючи команду make:test пакета SymfonyMakerBundle:

1
2
3
4
5
6
7
$ php bin/console make:test

 Який тип тесту ви хочете?:
 > WebTestCase

 Ім'я класу тесту (наприклад, BlogPostTest):
 > Controller\PostControllerTest

Це створює наступний клас тесту:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// tests/Controller/PostControllerTest.php
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class PostControllerTest extends WebTestCase
{
    public function testSomething(): void
    {
        // Викликає KernelTestCase::bootKernel(), та створює
        // "клієнта", який діє як браузер
        $client = static::createClient();

        // Запитати конкретну сторинку
        $crawler = $client->request('GET', '/');

        // Валідувати успішну відповідь і якийсь зміст
        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Hello World');
    }
}

У прикладі вище тест валідує, що HTTP-відповідь була успішною, і що тіло запиту містить тег <h1> з "Hello world".

Метод request() також повертає пошукового робота, якого ви можете використати, щоб створити складніші затвердження у ваших тестах:

1
2
3
4
$crawler = $client->request('GET', '/post/hello-world');

// наприклад, порахувати кількість елементів ``.comment`` на сторінці
$this->assertCount(4, $crawler->filter('.comment'));

Ви можете дізнатися більше про пошукового робота в Краулер DOM.

Створення запитів

Тестовий клієнт смилуює HTTP-клієнта як браузер і робить запити до вашого додатку Symfony:

1
$crawler = $client->request('GET', '/post/hello-world');

Метод request() бере HTTP-метод і URL в якості аргументів та повертає екземпляр Crawler.

Tip

Жорстке кодування URL запитів - краща практика для тестів додатків. Якщо тест генерує URL, використовуючи маршрутизатор Symfony, він не виявить змін, зроблених в URL додатку, що може вплинути на кінцевих користувачів.

Повний підпис методу request() наступний:

1
2
3
4
5
6
7
8
9
request(
    $method,
    $uri,
    array $parameters = [],
    array $files = [],
    array $server = [],
    $content = null,
    $changeHistory = true
)

Це дозволяє вам створювати всі мислимі типи запитів:

Tip

Тестовий клієнт доступний як сервіс test.client у контейнері в середовищі test (або там, де включена опція framework.test ). Це означає, що ви можете перевизначити сервіс повністю, якщо вам це знадобиться.

Декілька запитів в одному тесті

Після виконання запиту, наступні запити змусять клієнта перезавантажити ядро. Це перестворює контейнер з нуля, щоб забезпечити ізоляцію запитів і використання нових об'єктів сервісу кожного разу. Така поведінка може мати деякі неочікувані наслідки: наприклад, токен безпеки буде очищено, сутності Doctrine будуть від'єднані тощо.

По-перше, ви можете викликати метод клієнта disableReboot(), щоб скинути ядро замість його перезавантаження. На практиці, Symfony буде викликати метод reset() кожного сервісу з тегом kernel.reset. Однак, це також очистить токен безпеки, від'єднає сутності Doctrine тощо.

Щоб вирішити цю проблему, створіть :doc:`передачу компілятора </service_container/compiler_passes>', щоб
видалити тег kernel.reset з деяких сервісів у вашому тестовому середовищі:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/Kernel.php
namespace App;

use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel implements CompilerPassInterface
{
    use MicroKernelTrait;

    // ...

    protected function process(ContainerBuilder $container): void
    {
        if ('test' === $this->environment) {
            // запобігає очищенню токена безпеки
            $container->getDefinition('security.token_storage')->clearTag('kernel.reset');

            // запобігає відокремленню сутностей Doctrine
            $container->getDefinition('doctrine')->clearTag('kernel.reset');

            // ...
        }
    }
}

Перегляд сайту

Клієнт підтримує багато операцій, які можна зробити у реальному браузері:

1
2
3
4
5
6
$client->back();
$client->forward();
$client->reload();

// очищує всі кукі та історію
$client->restart();

Note

Методи back() і forward() пропускають перенаправлення, які могли виникнути при запиті URL, як роблять це звичайні браузери.

Перенаправлення

Коли запит повертає відповідь перенаправлення, клієнт не слідує йому автоматично. Ви можете дослідити відповідь та форсувати перенаправлення після цього методом followRedirect():

1
$crawler = $client->followRedirect();

Якщо ви хочете, щоб клієнт автоматично слідував усім перенаправленням, ви можете форсувати їх, викликавши метод followRedirects() до виконання запиту:

1
$client->followRedirects();

Якщо ви передасте false методу followRedirects(), перенаправлення більше не враховуватимуться:

1
$client->followRedirects(false);

Вхід користувачів у систему (аутентифікація)

Коли ви хочете додати тести додатку до захищеної сторінки, вам потрібно спочатку виконати "вхід" у якості користувача. Виконання необхідних кроів - таких як відправка форми входу в систему - робить тест дуже повільним. З цієї причини, Symfony надає метод loginUser(), щоб симулювати вхід у систему у ваших функціональних тестах.

Замість виконання входу реальними користувачами, рекомендується створити користувача лише для тестів. Ви можете зробити це за допомогою фікстур даних Doctrine, щоб завантажувати тестових користувачів лише у базах даних тестів.

Після завантаження користувачів у вашу базу даних, використайте сховище користувачів, щоб отримати цього користувача, та використайте $client->loginUser(), щоб зімітувати запит входу в систему:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// tests/Controller/ProfileControllerTest.php
namespace App\Tests\Controller;

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

class ProfileControllerTest extends WebTestCase
{
    // ...

    public function testVisitingWhileLoggedIn(): void
    {
        $client = static::createClient();
        $userRepository = static::getContainer()->get(UserRepository::class);

        // отримати тестового користувача
        $testUser = $userRepository->findOneByEmail('john.doe@example.com');

        // смулювати вхід $testUser у систему
        $client->loginUser($testUser);

        // тестувати, наприклад, сторінку профілю
        $client->request('GET', '/profile');
        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Hello John!');
    }
}

Ви можете передати будь-який екземпляр UserInterface методу loginUser(). Цей метод створює спеціальний об'єкт TestBrowserToken і зберігає його в сесії тестового клієнта.

Note

За проектом, метод loginUser() не працює при використанні брандмауерів без стану. Настомість, додайте відповідний токен/заголовок у кожному виклику request().

Виконання AJAX-запитів

Клієнт надає метод xmlHttpRequest(), який має ті ж аргументи, що і метод request(), і є скороченням, щоб робити AJAX-запити:

1
2
// необхідний заголовок HTTP_X_REQUESTED_WITH додається автоматично
$client->xmlHttpRequest('POST', '/submit', ['name' => 'Fabien']);

Відправка користувацький заголовків

Якщо ваш застосунок поводить себе у відповідності з деякими HTTP-заголовками, передайте їх в якості другого аргументу createClient():

1
2
3
4
$client = static::createClient([], [
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
]);

Ви також можете перевизначити HTTP-заголовки для кожного запиту:

1
2
3
4
$client->request('GET', '/', [], [], [
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
]);

Caution

Ім'я ваших користувацьких заголовків має слідувати синтаксису, визначеному у розділі 4.1.18 у RFC 3875: замініть - на _, перетворіть його у великі літери та додайте до результату префікс HTTP_. Наприклад, якщо ім'я вашого заголовка - X-Session-Token, передайте HTTP_X_SESSION_TOKEN.

Звіти про виключення

Виключення налагодження у тестах додатку можуть бути складними, так як за замовчуванням вони виявляються і вам необхідно переглянути логи, щоб побачити, яке виключення було викликано. Відключення кешування виключень у тестовому клієнті дозволяє звітувати про виключення через PHPUnit:

1
$client->catchExceptions(false);

Доступ до внутрішніх об'єктів

Якщо ви використовуєте клієнта, щоб тестувати ваш застосунок, ви можете захотіти отримати доступ до внутрішніх об'єктів клієнта:

1
2
$history = $client->getHistory();
$cookieJar = $client->getCookieJar();

Ви також можете отримати об'єкти, пов'язані з останнім запитом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// екземпляр запиту HttpKernel
$request = $client->getRequest();

// екземпляр запиту BrowserKit
$request = $client->getInternalRequest();

// екземпляр відповіді HttpKernel
$response = $client->getResponse();

// екземпляр відповіді BrowserKit
$response = $client->getInternalResponse();

// екземпляр Crawler
$crawler = $client->getCrawler();

Доступ до даних профілювальника

По кожному запиту ви можете підключити профільувальник Symfony, щоб збирати дані про внутрішню обробку цього запиту. Наприклад, профільувальник може бути використаний для верифікації, що задана сторінка робить менше, ніж визначена кількість запитів до бази даних, при завантаженні.

Шоб отримати профільувальника для останнього запиту, зробіть наступне:

1
2
3
4
5
6
7
// вмикає профільувальника для всіх наступних запитів
$client->enableProfiler();

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

// отримує профіль
$profile = $client->getProfile();

Щоб дізнатися особливі деталі використання профільувальника всередині тесту, див. статтю Як використовувати профілювальник у функціональному тесті.

Взаємодія з відповіддю

Як і реальний браузер, об'єкти Клієнта та Пошукового робота можуть бути використані для взаємодії зі сторінкою, яку вам видають:

Натискання на посилання

Використовуйте метод clickLink(), щоб натиснути на перше посилання, що містить заданий текст (або перше клікабельне повідомлення з атрибутом alt):

1
2
3
4
$client = static::createClient();
$client->request('GET', '/post/hello-world');

$client->clickLink('Click here');

Якщо вам потрібно отримати доступ до об'єкта Link, який надає корисні методи, що відносяться до посилань, таких як getMethod() та getUri()), використайте замість цього метод Crawler::selectLink():

1
2
3
4
5
6
7
8
$client = static::createClient();
$crawler = $client->request('GET', '/post/hello-world');

$link = $crawler->selectLink('Click here')->link();
// ...

// використайте click(), якщо ви хочете натиснути на обране посилання
$client->click($link);

Відправка форм

Використовуйте метод submitForm(), щоб відправити форму, що містить дану кнопку:

1
2
3
4
5
6
$client = static::createClient();
$client->request('GET', '/post/hello-world');

$crawler = $client->submitForm('Add comment', [
    'comment_form[content]' => '...',
]);

Перший аргумент submitForm() - текстовий зміст, id, value або name будь-якого <button> або <input type="submit">, доданого у форму. Другий необов'язковий аргумент використовується для перевизначення значень полів форми за замовчуванням.

Note

Відмітьте, що ви маєте обрати кнопки форми, а не форми, так як форма може мати декілька кнопок. Якщо ви використовуєте траверсуючий API, пам'ятайте, що ви маєте шукати кнопку.

Якщо вам потрібно отримати доступ до об'єкта Form, який надає корисні методи спеціально для форм (так як getUri(), getValues() і getFields()), використайте замість цього метод Crawler::selectButton():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$client = static::createClient();
$crawler = $client->request('GET', '/post/hello-world');

// обрати кнопку
$buttonCrawlerNode = $crawler->selectButton('submit');

// отримати об'єкт Форми для форми, що належить до цієї кнопки
$form = $buttonCrawlerNode->form();

// встановити значення у об'єкті форми
$form['my_form[name]'] = 'Fabien';
$form['my_form[subject]'] = 'Symfony rocks!';

// відправити об'єкт Форми
$client->submit($form);

// за бажанням, ви можете об'єднати 2 останніх кроки, передавши масив значень
// поля при відправці форми:
$client->submit($form, [
    'my_form[name]'    => 'Fabien',
    'my_form[subject]' => 'Symfony rocks!',
]);

В залежності від типу форми, ви можете використовувати різні методи, щоб заповнити введення:

1
2
3
4
5
6
7
8
9
10
11
12
// обирає опцію або радіо-кнопку
$form['my_form[country]']->select('France');

// відмічає чекбокс
$form['my_form[like_symfony]']->tick();

// завантажує файл
$form['my_form[photo]']->upload('/path/to/lucas.jpg');

// У випадку завантаження декількох файлів
$form['my_form[field][0]']->upload('/path/to/lucas.jpg');
$form['my_form[field][1]']->upload('/path/to/lisa.jpg');

Tip

Замість жорсткого кодування імені форми як частини імен полів (наприклад, my_form[...] у попередньому прикладі), ви можете використати метод getName(), щоб отримати ім'я форми.

Tip

Якщо ви спеціально хочете обрати значення кнопок радіо/вибору "invalid", див. .

Tip

Ви можете отримати значення, які будуть відправлені шляхом виклику методу getValues() в об'єкті Form. Завантажені файли доступні в окремому масиві, поверненому getFiles(). Методи getPhpValues() та getPhpFiles() також повертають відправлені дані, але у PHP-форматі (він конвертує ключі з нотацією квадратними дужками - наприклад, my_form[subject] - у PHP-масиви).

Tip

Методи submit() і submitForm() визначають необов'язкові аргументи для додавання користувацьких параметрів сервісу та HTTP-заголовків при відправленні форми:

1
2
$client->submit($form, [], ['HTTP_ACCEPT_LANGUAGE' => 'es']);
$client->submitForm($button, [], 'POST', ['HTTP_ACCEPT_LANGUAGE' => 'es']);

Тестування відповідей (cтвердження)

Тепер, коли тести відвідали сторінку та взаємодіяли з нею (наприклад, заповнювали форму), час верифікувати, що відображається очікуване виведення.

Так як всі тести знаходяться в PHPUnit, ви можете використати будь-яке cтвердження PHPUnit у ваших тестах. Разом з Клієнтом та Пошуковим роботом це дозволяє вам перевірити все, що ви хочете.

Однак, Symfony надає зручні методи скорочень для більшості розповсюджених випадків:

Твердження відповідей

assertResponseIsSuccessful(string $message = '')
Стверджує, що відпвідь була успішною (HTTP-статус 2xx).
assertResponseStatusCodeSame(int $expectedCode, string $message = '')
Стверджує конкретний HTTP статус-код.
assertResponseRedirects(string $expectedLocation = null, int $expectedCode = null, string $message = '')
Стверджує, що відповідь - це відповідь перенаправлення (за бажанням, ви можете перевірити цільову локацію та статус-код).
assertResponseHasHeader(string $headerName, string $message = '')/assertResponseNotHasHeader(string $headerName, string $message = '')
Стверджує, що заданий заголовок (не)доступний у відповіді.
assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = '')/assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = '')
Стверджує, що заданий заголовок (не) містить очікуваного значення у відповіді.
assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')/assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')
Стверджує, що заданий кукі є у відповіді (за бажанням, перевіряє шлях або домен конкретного кукі).
assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = '')
Стверджує, що заданий кукі існує та встановлює очікуване значення.
assertResponseFormatSame(?string $expectedFormat, string $message = '')
Стверджує, що формат поверненої методом getFormat() відповіді такий же, як і очікуване значення.
assertResponseIsUnprocessable(string $message = '')
Стверджує, що відповідь неможливо обробити (HTTP-статус 422)

Твердження запитів

assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = '')
Стверджує, що заданий атрибут запиту встановлений в очікуваному значенні.
assertRouteSame($expectedRoute, array $parameters = [], string $message = '')
Стверджує відповідь, співставлену із заданим маршрутом і, за бажанням, параметри маршруту.

Твердження браузера

assertBrowserHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')/assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = '')
Стверджує, що Клієнт тесту (не) має встановленого заданого кукі (що означає, що кукі було встановлено будь-яким запитом у тесті).
assertBrowserCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = '')
Стверджує, що заданий кукі у тестовому Клієнте встановлений в очікуваному значенні.
assertThatForClient(Constraint $constraint, string $message = '')

Стверджує задане Обмеження в Клієнті. Корисно для використання користувацьких затверджень так само, як і вбудованих переконань (тобто без передаці Клієнту в якості аргументу):

1
2
3
4
5
// додайте цей метод в деякий користувацький клас, імпортований у ваші тести
protected static function assertMyOwnCustomAssert(): void
{
    self::assertThatForClient(new SomeCustomConstraint());
}

Твердження пошукових роботів

assertSelectorExists(string $selector, string $message = '')/assertSelectorNotExists(string $selector, string $message = '')
Стверджує, що заданий cелектор (не) співпадає як мінімум з одним елементом у відповіді.
assertSelectorCount(int $expectedCount, string $selector, string $message = '')
Стверджує, що очікувана кількість елементів селектору знаходиться у відповіді
assertSelectorTextContains(string $selector, string $text, string $message = '')/assertSelectorTextNotContains(string $selector, string $text, string $message = '')
Стверджує, що перший елемент, що співпадає з заданим селектором, (не) містить очікуваний текст.
assertSelectorTextSame(string $selector, string $text, string $message = '')
Стверджує, що зміст першого елементу, що співпадає із заданим селектором, (не) дорівнює очікуваному тексту.
assertPageTitleSame(string $expectedTitle, string $message = '')
Стверджує, що елемент <title> дорівнює заданому заголовку.
assertPageTitleContains(string $expectedTitle, string $message = '')
Стверджує, що елемент <title> містить заданий заголовок.
assertInputValueSame(string $fieldName, string $expectedValue, string $message = '')/assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = '')
Стверджує, що значення введення форми з заданим іменем (не) дорівнює очікуваному значенню.
assertCheckboxChecked(string $fieldName, string $message = '')/assertCheckboxNotChecked(string $fieldName, string $message = '')
Стверджує, що чекбокс із заданим іменем (не) відмічено.
assertFormValue(string $formSelector, string $fieldName, string $value, string $message = '')/assertNoFormValue(string $formSelector, string $fieldName, string $message = '')
Стверджує, що значення поля першої форми, що співпадає з заданим селектором, (не) дорівнює очікуваному значенню.

Твердження Mailer

assertEmailCount(int $count, string $transport = null, string $message = '')
Стверджує, що була відправлена очікувана кількість електронних листів.
assertQueuedEmailCount(int $count, string $transport = null, string $message = '')
Стверджує, що очікувана кількість електронних листів була поставлена у чергу (наприклад, використовуючи компонент Messenger).
assertEmailIsQueued(MessageEvent $event, string $message = '')/assertEmailIsNotQueued(MessageEvent $event, string $message = '')
Стверджує, що задана подія поштової програми (не) в черці. Використайте getMailerEvent(int $index = 0, string $transport = null), щоб отримати подію поштової програми за індексом.
assertEmailAttachmentCount(RawMessage $email, int $count, string $message = '')
Стверджує, що заданий електронний лист має очікувану кількість вкладень. Використайте getMailerMessage(int $index = 0, string $transport = null), щоб отримати конкретний лист за індексом.
assertEmailTextBodyContains(RawMessage $email, string $text, string $message = '')/assertEmailTextBodyNotContains(RawMessage $email, string $text, string $message = '')
Стверджує, що тіло тексту заданого листа (не) містить очікуваний текст.
assertEmailHtmlBodyContains(RawMessage $email, string $text, string $message = '')/assertEmailHtmlBodyNotContains(RawMessage $email, string $text, string $message = '')
Стверджує, що HTML-тіло заданого листа (не) містить очікуваний текст.
assertEmailHasHeader(RawMessage $email, string $headerName, string $message = '')/assertEmailNotHasHeader(RawMessage $email, string $headerName, string $message = '')
Стверджує, що заданий лист (не) має очікуваного встановленого заголовка.
assertEmailHeaderSame(RawMessage $email, string $headerName, string $expectedValue, string $message = '')/assertEmailHeaderNotSame(RawMessage $email, string $headerName, string $expectedValue, string $message = '')
Стверджує, що заданий лист (не) має очікуваного заголовка, встановленого в очікуваному значенні.
assertEmailAddressContains(RawMessage $email, string $headerName, string $expectedValue, string $message = '')
Стверджує, що заданий заголовок адреси дорівнює очікуваній адресі електронної пошти. Це затвердження нормалізує адреси на кшталт Jane Smith <jane@example.com> у jane@example.com.

Твердження Notifier

assertNotificationCount(int $count, string $transportName = null, string $message = '')
Стверджує, що задана кількість сповіщень була створена (загалом або для заданого транспорту).
assertQueuedNotificationCount(int $count, string $transportName = null, string $message = '')
Стверджує, що задана кількість сповіщень знаходиться у черзі (загалом або для заданого транспорту).
assertNotificationIsQueued(MessageEvent $event, string $message = '')
Стверджує, що задане сповіщення у черзі.
assertNotificationIsNotQueued(MessageEvent $event, string $message = '')
Стверджує, що задане сповіщення не у черзі.
assertNotificationSubjectContains(MessageInterface $notification, string $text, string $message = '')
Стверджує, що заданий текст включено до субʼєкта заданого сповіщення.
assertNotificationSubjectNotContains(MessageInterface $notification, string $text, string $message = '')
Стверджує, що заданий текст не включено до субʼєкта заданого сповіщення.
assertNotificationTransportIsEqual(MessageInterface $notification, string $transportName, string $message = '')
Стверджує, що імʼя транспорту для заданого сповіщення таке саме як і заданий текст.
assertNotificationTransportIsNotEqual(MessageInterface $notification, string $transportName, string $message = '')
Стверджує, що імʼя транспорту для заданого сповіщення не таке саме як і заданий текст.

Твердження HttpClient

Tip

Для всіх наступних тверджень, $client->enableProfiler() має бути викликано перед кодом, який буде запускати HTTP-запит(и).

assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client')
Стверджує, що заданий URL було викликано з використанням, якщо вказано, тіла та заголовків методу. За замовчуванням перевіряється на HttpClient, але ви також можете передати конкретний ідентифікатор HttpClient. (Це спрацює, якщо запит було викликано декілька разів).
assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client')
Стверджує, що заданий URL не було викликано за допомогою GET або вказаного методу. За замовчуванням перевіряється на HttpClient, але можна вказати ідентифікатор HttpClient.
assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client')
Стверджує, що на HttpClient було зроблено задану кількість запитів. За замовчуванням перевіряється HttpClient, але ви також можете передати певний HttpClient ID.

Наскрізні тести (E2E)

Якщо вам потрібно протестувати додаток в цілому, включаючи код JavaScript, ви можете використовувати реальний браузер замість тестового клієнта. Це називається називається наскрізним тестом, і це чудовий спосіб протестувати додаток.

Цього можна досягти завдяки компоненту Panther. Ви можете дізнатися більше про нього на спеціальній сторінці.