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

Posted

技术标签:

【中文标题】如何使用 Symfony 表单和数据转换器实现测试隔离?【英文标题】:How to achieve test isolation with Symfony forms and data transformers? 【发布时间】:2017-02-20 06:19:48 【问题描述】:

注意:这是 Symfony

首先,请考虑这种将一个或多个实体表示为隐藏字段的表单类型(为简洁起见,省略了命名空间的内容)

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 表单的单元测试是集成测试,它们应该确保转换器不会失败。即使这样说 in the documentation!”

此测试检查表单未使用任何数据转换器 失败的。 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,
            ));
    

    /* ... */

现在进入问题。 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()
        )
    );

看到评论在中间的什么位置了吗?是的,因此为了使其正常工作,HiddenEntityType 的单元测试类中的所有模拟和期望现在都需要在此处有效地复制。我对此不满意,那我有什么选择?

    注入变压器作为选项之一

    这将非常简单,并且会使模拟变得更简单,但最终只会把罐子踢到路上。因为在这种情况下,new EntityToPrimaryKeyTransformer() 只会从一种表单类型类移动到另一种。更不用说我觉得表单类型应该对系统的其他部分隐藏它们的内部复杂性。此选项意味着将复杂性推到表单类型的边界之外。

    在表单类型中注入各种变压器工厂

    这是一种更典型的从方法中删除“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 */
    
    

    在我考虑工厂的实际外观之前,这感觉还不错。首先,它需要注入实体管理器。但那又如何呢?如果我往前看,这个所谓的通用工厂可能需要各种依赖项来创建不同类型的数据转换器。这显然不是一个好的长期设计决策。那么呢?将其重新标记为EntityManagerAwareDataTransformerFactory?这里开始有点乱了。

    我没想到的东西...

想法?经验?中肯的建议?

【问题讨论】:

【参考方案1】:

首先,我几乎没有使用 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']
        );
    

现在进行测试,您创建一个新类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();
        /* ... */
    


【讨论】:

感谢您的回复,更感谢您明确推荐的来源。将新书添加到阅读列表中总是很不错的。

以上是关于如何使用 Symfony 表单和数据转换器实现测试隔离?的主要内容,如果未能解决你的问题,请参考以下文章

将参数传递给 symfony 5.4 表单测试不起作用

数据转换器与约束

Symfony 表单:HTML5 数据列表

Symfony 5 表单通知:App\Entity\Epic 类的对象无法转换为 int

symfony如何知道从表单中使用哪个实体

在持久化到实体之前转换从表单收集的数据