Тестирование

Дата обновления перевода 2024-08-01

Тестирование

Как только вы пишете новую строку кода, вы также потенциально добавляете новые ошибки. Для того чтобы создавать более надёжные приложения, вы должны тестировать их, используя как функциональные, так и модульные (unit) тесты.

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

В Symfony интегрирована независимая библиотека под названием PHPUnit, которая предоставляет вам богатый тестовый фреймворк. Эта статья не покрывает все нюансы PHPUnit, но у него есть своя подробная документация.

Перед установкой вашего первого теста, установите symfony/test-pack, который устанавливает некоторые другие пакеты, необходимые для тестирования (такие как phpunit/phpunit):

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

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

1
$ php 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
// ...
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(
    string $method,
    string $uri,
    array $parameters = [],
    array $files = [],
    array $server = [],
    string $content = null,
    bool $changeHistory = true
): Crawler

Это позволяет вам создавать все мыслимые типы запросов:

Tip

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

Несколько запросов в одном тесте

После выполнения запроса последующие запросы будут заставлять клиента перезагружать ядро.
При этом контейнер создается заново, чтобы гарантировать, что запросы изолированы и каждый раз используют новые объекты сервиса. Такое поведение может привести к неожиданным последствия: например, токен безопасности будет очищен, объекты Doctrine будут отсоединены и т. д.

Во-первых, вы можете вызвать клиентский метод
disableReboot(), чтобы сбросить настройки ядра вместо его перезагрузки. На практике Symfony будет вызывать метод reset() каждого сервиса, помеченного тегом kernel.reset. Однако при этом также очищается токен безопасности, отсоединяются сущности Doctrine и т.д.

Чтобы решить эту проблему, создайте передачу компилятора для удаления тега 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) {
            // prevents the security token to be cleared
            $container->getDefinition('security.token_storage')->clearTag('kernel.reset');

            // prevents Doctrine entities to be detached
            $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 и хранит его в сессии тестового клиента. Если вам необходимо определить пользовательские
атрибуты в этом токене, вы можете использовать аргумент tokenAttributes метода loginUser().

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']);

Тестирование ответов (утверждения)

Теперь, когда тесты посетили страницу и взаимодействовали с ней (например, заполняли форму), пора верифицировать, что отображается ожидаемый вывод.

Так как все тесты находятся в PHPUnit, вы можете использовать любое утверждение 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());
}

Утверждения Crawler

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 = '')
Утверждает, что содержание первого элемента, совпадающего с заданным селектором (не) равняется ожидаемому тексту.
assertSelectorTextSame(string $selector, string $text, string $message = '')
Утверждает, что содержание первого элемента, соответствующего заданному
селектору, равно ожидаемому тексту
assertAnySelectorTextSame(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.
assertEmailSubjectContains(RawMessage $email, string $expectedValue, string $message = '')/assertEmailSubjectNotContains(RawMessage $email, string $expectedValue, string $message = '')
Утверждает, что тема заданного письма содержит (не содержит) ожидаемую тему.

Утверждения 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.

Сквозные тесты (E2E)

Если вам нужно протестировать приложение в целом, включая код JavaScript, то вместо тестового клиента можно использовать реальный браузер. Это называется сквозным тестированием, и это отличный способ протестировать приложение.

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