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

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

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

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

Note

В зависимости от того, как вы установили вашу Symfony или компонент Формы, тесты могут быть не скачаны. В таком случае, используйте опцию --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
// 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 = array(
            'test' => 'test',
            'test2' => 'test2',
        );

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

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

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

        $this->assertTrue($form->isSynchronized());
        $this->assertEquals($object, $form->getData());

        $view = $form->createView();
        $children = $view->children;

        foreach (array_keys($formData) as $key) {
            $this->assertArrayHasKey($key, $children);
        }
    }
}

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

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

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

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

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

Note

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

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

1
$this->assertEquals($object, $form->getData());

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

1
2
3
4
5
6
$view = $form->createView();
$children = $view->children;

foreach (array_keys($formData) as $key) {
    $this->assertArrayHasKey($key, $children);
}

Тестирование типов из сервис-контейнера

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

Чтобы решить это, вам нужно сымитировать внедрённые зависимости, инстанциировать ваш собственный тип формы и использовать 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
// tests/Form/Type/TestedTypeTest.php
namespace App\Tests\Form\Type;

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

class TestedTypeTest extends TypeTestCase
{
    private $entityManager;

    protected function setUp()
    {
        // симитировать любые зависимости
        $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);

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

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

Часто случается так, что вы используете какие-то опции, добавленные расширениями формы. Одним из случаев может быть 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
27
28
29
30
31
32
33
34
// tests/Form/Type/TestedTypeTest.php
namespace App\Tests\Form\Type;

// ...
use App\Form\Type\TestedType;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\Form;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class TestedTypeTest extends TypeTestCase
{
    private $validator;

    protected function getExtensions()
    {
        $this->validator = $this->createMock(ValidatorInterface::class);
        // использовать getMock() в PHPUnit 5.3 или более ранних версиях
        // $this->validator = $this->getMock(ValidatorInterface::class);
        $this->validator
            ->method('validate')
            ->will($this->returnValue(new ConstraintViolationList()));
        $this->validator
            ->method('getMetadataFor')
            ->will($this->returnValue(new ClassMetadata(Form::class)));

        return array(
            new ValidatorExtension($this->validator),
        );
    }

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

Также возможно загружать пользовательские типы форм, расширения типов форм или отгадыватели типов, используя методы getTypes(), :method:`Symfony\Component\Form\Test\FormIntegrationTestCase::`getTypeExtensions и getTypeGuessers().

Тестирование с разными наборами данных

Если вы ещё не знакомы с поставщиками данных PHPUnit, это может стать хорошим случаем, чтобы их использовать:

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

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

class TestedTypeTest extends TypeTestCase
{
    /**
     * @dataProvider getValidTestData
     */
    public function testForm($data)
    {
        // ... your test
    }

    public function getValidTestData()
    {
        return array(
            array(
                'data' => array(
                    'test' => 'test',
                    'test2' => 'test2',
                ),
            ),
            array(
                'data' => array(),
            ),
            array(
                'data' => array(
                    'test' => null,
                    'test2' => null,
                ),
            ),
        );
    }
}

Код выше запустит ваши тесты три раза с 3 разными наборами данных. Это позволяет разделить тест над общим набором объектов и тесты, а также облегчает тестирование с несколькими наборами данных.

Вы также можете передать другой аргумент, например, булев, если форма должна быть синхроизирована с данными набором данных или нет, и т.д.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.