Подтвердить что ты не робот

Как добиться изоляции теста с помощью форм Symfony и трансформаторов данных?

Примечание: Это Symfony < 2.6, но я считаю, что одна и та же общая проблема применяется независимо от версии

Чтобы начать, рассмотрим этот тип формы, который предназначен для представления одного или нескольких объектов в виде скрытого поля (сокращение пространства имен для краткости)

class HiddenEntityType extends AbstractType
{
    /**
     * @var EntityManager
     */
    protected $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        if ($options['multiple']) {
            $builder->addViewTransformer(
                new EntitiesToPrimaryKeysTransformer(
                    $this->em->getRepository($options['class']),
                    $options['get_pk_callback'],
                    $options['identifier']
                )
            );
        } else {
            $builder->addViewTransformer(
                new EntityToPrimaryKeyTransformer(
                    $this->em->getRepository($options['class']),
                    $options['get_pk_callback']
                )
            );
        }
    }

    /**
     * See class docblock for description of options
     *
     * {@inheritdoc}
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'get_pk_callback' => function($entity) {
                return $entity->getId();
            },
            'multiple' => false,
            'identifier' => 'id',
            'data_class' => null,
        ));

        $resolver->setRequired(array('class'));
    }

    public function getName()
    {
        return 'hidden_entity';
    }

    /**
     * {@inheritdoc}
     */
    public function getParent()
    {
        return 'hidden';
    }
}

Это работает, это просто и по большей части выглядит как все примеры, которые вы видите для добавления трансформаторов данных в тип формы. Пока вы не дойдете до модульного тестирования. См. Проблему? Трансформаторы не могут насмехаться. "Но ждать!" вы говорите: "Модульные тесты для форм Symfony - это интеграционные тесты, они должны быть уверены, что трансформаторы не сбой. Даже говорит, что в документации!"

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

Хорошо, тогда вы живете с тем фактом, что вы не можете изолировать трансформаторы. Нет большой сделки?

Теперь рассмотрим, что происходит при модульном тестировании формы с полем этого типа (предположим, что HiddenEntityType был определен и помечен в контейнере службы)

class SomeOtherFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('field', 'hidden_entity', array(
                'class' => 'AppBundle:EntityName',
                'multiple' => true,
            ));
    }

    /* ... */
}

Теперь возникает проблема. unit test для SomeOtherFormType теперь необходимо реализовать getExtensions(), чтобы использовать тип hidden_entity. Итак, как это выглядит?

protected function getExtensions()
{
    $mockEntityManager = $this
        ->getMockBuilder('Doctrine\ORM\EntityManager')
        ->disableOriginalConstructor()
        ->getMock();

    /* Expectations go here */

    return array(
        new PreloadedExtension(
            array('hidden_entity' => new HiddenEntityType($mockEntityManager)),
            array()
        )
    );
}

Посмотрите, где этот комментарий посередине? Да, поэтому для правильной работы все издевательства и ожидания, которые находятся в классе unit test для HiddenEntityType, теперь теперь должны быть дублированы здесь. Я не в порядке с этим, так что мои варианты?

  • Вставьте трансформатор в качестве одной из опций

    Это было бы очень просто и сделало бы насмешкой более простым, но в итоге просто ударил бы по дороге вниз. Потому что в этом случае new EntityToPrimaryKeyTransformer() просто переместится из одного класса типа формы в другой. Не говоря уже о том, что я чувствую, что типы форм должны скрывать свою внутреннюю сложность от остальной системы. Этот параметр означает, что эта сложность выходит за пределы формы формы.

  • Внесите трансформатор factory сортировки в тип формы

    Это более типичный подход к удалению "новых возможностей" из метода, но я не могу поколебать ощущение, что это делается только для того, чтобы сделать код пригодным для тестирования, и на самом деле не делает код лучше. Но если это будет сделано, это будет выглядеть примерно так.

    class HiddenEntityType extends AbstractType
    {
        /**
         * @var DataTransformerFactory 
         */
        protected $transformerFactory;
    
        public function __construct(DataTransformerFactory $transformerFactory)
        {
            $this->transformerFactory = $transformerFactory;
        }
    
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder->addViewTransformer(
                $this->transformerFactory->createTransfomerForType($this, $options);
            );
        }
    
        /* Rest of type unchanged */
    }
    

    Это нормально, пока я не рассмотрю, как будет выглядеть factory. Для начала понадобится менеджер организации. Но что тогда? Если я буду смотреть дальше по дороге, этому предположительно-родовому factory могут понадобиться всевозможные зависимости для создания трансформаторов данных разных типов. Это явно не очень хорошее долгосрочное дизайнерское решение. Итак, что? Повторно назовите это как EntityManagerAwareDataTransformerFactory? Это начинает чувствовать себя грязным здесь.

  • Материал, о котором я не думаю...

Мысли? Опыт? Твердые советы?

4b9b3361

Ответ 1

Прежде всего, у меня нет опыта работы с Symfony. Однако, я думаю, вы пропустили третий вариант. В работе "Эффективно с устаревшим кодом" Майкл Фейрс описывает способ изолировать зависимости, используя наследование (он называет это "Извлечение и переопределение" ).

Это происходит следующим образом:

class HiddenEntityType extends AbstractType
{
    /* stuff */

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        if ($options['multiple']) {
            $builder->addViewTransformer(
                $this->createEntitiesToPrimaryKeysTransformer($options)
            );
        }
    }

    protected function createEntitiesToPrimaryKeysTransformer(array $options)
    {
        return new EntitiesToPrimaryKeysTransformer(
            $this->em->getRepository($options['class']),
            $options['get_pk_callback'],
            $options['identifier']
        );
    }
}

Теперь для тестирования вы создаете новый класс FakeHiddenEntityType, который расширяет HiddenEntityType.

class FakeHiddenEntityType extends HiddenEntityType {

    protected function createEntitiesToPrimaryKeysTransformer(array $options) {
        return $this->mock;
    }    

}

Где $this->mock, очевидно, все, что вам нужно.

Двумя наиболее важными преимуществами являются отсутствие каких-либо заводов, поэтому сложность все еще инкапсулирована, и практически нет никаких шансов, что это изменение нарушит существующий код.

Недостатком является то, что этот метод требует дополнительного класса. Что еще более важно, для этого требуется класс, который знает о внутренних компонентах тестируемого класса.


Чтобы избежать лишнего класса или, скорее, скрыть дополнительный класс, можно инкапсулировать его в функцию, создав вместо этого анонимный класс (поддержка анонимных классов была добавлена ​​в PHP 7).

class HiddenEntityTypeTest extends TestCase
{

    private function createHiddenEntityType()
    {
        $mock = ...;  // Or pass as an argument

        return new class extends HiddenEntityType {

            protected function createEntitiesToPrimaryKeysTransformer(array $options)
            {
                return $mock;
            }    

        }
    }

    public function testABC()
    {
        $type = $this->createHiddenEntityType();
        /* ... */
    }

}