如何使用Symfony表单和数据转换器实现测试隔离?

Pet*_*ley 20 php unit-testing symfony-forms 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';
    }
}
Run Code Online (Sandbox Code Playgroud)

这是有效的,它很简单,并且大部分看起来就像您看到的将数据转换器添加到表单类型的所有示例.直到你进行单元测试.看到问题?变形金刚不能被嘲笑."可是等等!" 你说,"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,
            ));
    }

    /* ... */
}
Run Code Online (Sandbox Code Playgroud)

现在输入问题.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()
        )
    );
}
Run Code Online (Sandbox Code Playgroud)

看看评论中间的位置?是的,为了使其正常工作,HiddenEntityType现在有效地需要在单元测试类中的所有模拟和期望 重复.我不喜欢这个,所以我的选择是什么?

  1. 注入变压器作为选项之一

    这将是非常简单的,并会使嘲弄更简单,但最终只是踢了罐头.因为在这种情况下,new EntityToPrimaryKeyTransformer()只会从一个表单类型类移动到另一个表单类型类.更不用说我觉得表单类型应该隐藏其内部复杂性与系统的其他部分.此选项意味着将该复杂性推到表单类型的边界之外.

  2. 将各种变压器工厂注入表单类型

    这是一种从方法中删除"newables"的更典型的方法,但我不能感觉这样做只是为了使代码可测试,并且实际上并没有使代码更好.但如果这样做了,它会看起来像这样

    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 */
    }
    
    Run Code Online (Sandbox Code Playgroud)

    在我考虑工厂的实际外观之前,这感觉还不错.对于初学者来说,它需要注入实体经理.那么呢?如果我继续向前看,这个所谓的通用工厂可能需要各种依赖关系来创建不同类型的数据转换器.这显然不是一个好的长期设计决定.那么什么呢?重新标记为EntityManagerAwareDataTransformerFactory?在这里开始感到凌乱.

  3. 东西我没想到......

思考?经验?坚实的建议?

Pet*_*ete 12

首先,我接下来没有Symfony的经验.但是,我认为你错过了第三种选择.在有效地使用遗留代码时,Michael Feathers概述了一种通过使用继承来隔离依赖关系的方法(他将其称为"提取和覆盖").

它是这样的:

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']
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

现在进行测试,您将创建一个新类FakeHiddenEntityType,即扩展HiddenEntityType.

class FakeHiddenEntityType extends HiddenEntityType {

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

}
Run Code Online (Sandbox Code Playgroud)

其中$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();
        /* ... */
    }

}
Run Code Online (Sandbox Code Playgroud)