Как проводить модульное тестирование ваших форм

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

Как проводить модульное тестирование ваших форм

Caution

Эта статья предназначена для разработчиков, которые создают пользовательские типы форм. Если вы используете встроенные типы форм Symfony, или типы форм, предоставленные сторонними пакетами, вам не нужно проводить их модульное тестирование.

Компонент Form состоит из 3 базовых объектов: типа формы (реализующего FormTypeInterface), Form и FormView.

Единственный класс, который обычно изменяют программисты - это класс типа формы, который служит схемой формы. Он используется для генерирования Form и FormView. Вы можете протестировать его напрямую сымитировав его взаимодействия с фабрикой, но это будет сложно. Лучше передать его в FormFactory так, как это делается в настоящем приложении. Его легко использовать в начальной загрузке, и вы можете доверять компонентам Symfony достаточно, чтобы использовать их в качестве базы для тестирования.

Уже существует класс, от которого вы можете получить пользу в тестировании: TypeTestCase. Он используется для тестирования базовых типов и вы можете исползовать его для тестирования собственных типов.

Note

В зависимости от того, как вы установили вашу Symfony или компонент Form, тесты могут быть не скачаны. В таком случае, используйте опцию --prefer-source в Composer.

Основы

Простейшая реализация TypeTestCase выглядит так:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// tests/Form/Type/TestedTypeTest.php
namespace App\Tests\Form\Type;

use App\Form\Type\TestedType;
use App\Model\TestObject;
use Symfony\Component\Form\Test\TypeTestCase;

class TestedTypeTest extends TypeTestCase
{
    public function testSubmitValidData()
    {
        $formData = [
            'test' => 'test',
            'test2' => 'test2',
        ];

        $model = new TestObject();
        // $model извлечет данные из отправки формы; передать в качестве второго аргумента
        $form = $this->factory->create(TestedType::class, $model);

        $expected = new TestObject();
        // ...наполнить свойства $object данными, хранящимися в $formData

        // отправить данные в форму напрямую
        $form->submit($formData);

        // Эта проверка гарантирует, что ошибки преобразования отсутствуют
        $this->assertTrue($form->isSynchronized());

        // проверить, что $model был изменен, как ожидалось, когда форма была отправлена
        $this->assertEquals($expected, $model);
    }

    public function testCustomFormView()
    {
        $formData = new TestObject();
        // ... подготовить данные, как вам нужно

        // Изначальные данные могут быть использованы для вычисления переменных пользовательского просмотора
        $view = $this->factory->create(TestedType::class, $formData)
            ->createView();

        $this->assertArrayHasKey('custom_var', $view->vars);
        $this->assertSame('expected value', $view->vars['custom_var']);
    }
}

Итак, что это тестирует? Вот детальное разъясение.

Во-первых, вы верифиуируете компиляцию FormType. Это включает в себя базовое наследование классов, функцию buildForm() и резолюцию опций. Это должно быть первым тестом, который вы напишете:

1
$form = $this->factory->create(TestedType::class, $formData);

Этот тест проверяет, чтобы ни один из ваших преобразователей данных, использованных формой, не потерпели неудачу. Метод isSynchronized() устаналивается, как false только, если преобразователь данных выдаёт исключение:

1
2
$form->submit($formData);
$this->assertTrue($form->isSynchronized());

Note

Не тестируйте валидацию: она применяется слушателем, который не активен в случае тестирования и полагается на конфигурацию валидации. Вместо этого, проведите модульное тестирование ваших пользовательских ограничений напрямую, или прочтите, как добавлять пользовательские расширения в последнем разделе этой страницы.

Далее, верифицируйте отправку и отображение формы. Тест ниже проверяет, правильно ли были указаны все поля:

1
$this->assertEquals($expected, $formData);

Наконец, проверьте создание FormView. Вы должны проверить, все ли виджеты, которые вы хотите отобразить, доступны в дочернем свойстве:

1
2
$this->assertArrayHasKey('custom_var', $view->vars);
$this->assertSame('expected value', $view->vars['custom_var']);

Tip

Используйте поставщики данных PHPUnit, чтобы тестировать условия нескольких форм, используя один и тот же код тестирования.

Caution

Если ваш тип полагается на EntityType, вы должны зарегистрировать DoctrineOrmExtension, который должен будет имитировать ManagerRegistry.

Однако, если вы не можете использовать имитацию для написания своего теста, вам нужно вместо этого расширить KernelTestCase, и использовать сервис form.factory, чтоы создать форму.

Тестирование типов, зарегистрированных, как сервисы

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

Чтобы решить это, вам нужно сымитировать внедрённые зависимости, инстанциировать ваш собственный тип формы и использовать PreloadedExtension, чтобы убедиться, что FormRegistry использует созданный экземпляр:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// tests/Form/Type/TestedTypeTest.php
namespace App\Tests\Form\Type;

use App\Form\Type\TestedType;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
// ...

class TestedTypeTest extends TypeTestCase
{
    private $objectManager;

    protected function setUp(): void
    {
        // сымитировать любые зависимости
        $this->entityManager = $this->createMock(ObjectManager::class);

        parent::setUp();
    }

    protected function getExtensions()
    {
        // создать экземпляр типа с имимтированными зависимостями
        $type = new TestedType($this->entityManager);

        return array(
            // зарегистрировать экземляры типов в PreloadedExtension
            new PreloadedExtension(array($type), array()),
        );
    }

    public function testSubmitValidData()
    {
        // ...

        // Вместо создания нового экземпляра, будет использован созданный в
        // getExtensions().
        $form = $this->factory->create(TestedType::class, $formData);

        // ... ваш тест
    }
}

Добавление пользовательских расширений

Часто случается так, что вы используете какие-то опции, добавленные расширениями формы. Одним из случаев может быть ValidatorExtension с его опцией invalid_message. TypeTestCase загружает только базовое расширение формы, что означает, что InvalidOptionsException будет вызван, если вы попробуете протестировать класс, зависящий от других расширений. Метод getExtensions() позволяет вам возвращать список расширений для регистрации:

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
// tests/Form/Type/TestedTypeTest.php
namespace App\Tests\Form\Type;

// ...
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Validator\Validation;

class TestedTypeTest extends TypeTestCase
{
    protected function getExtensions()
    {
        $validator = Validation::createValidator();

        // или, если вам также нужно читать ограничения из аннотаций
        $validator = Validation::createValidatorBuilder()
            ->enableAnnotationMapping(true)
            ->addDefaultDoctrineAnnotationReader()
            ->getValidator();

        return [
            new ValidatorExtension($validator),
        ];
    }

    // ... ваши тесты
}

Note

По умолчанию, только CoreExtension зарегистрирован в тестах. Вы можете найти другие расширения из компонента Form в пространстве имен Symfony\Component\Form\Extension.

Также возможно загружать пользовательские типы форм, расширения типов форм или отгадыватели типов, используя методы getTypes(), getTypeExtensions() и getTypeGuessers().