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

时间:2016-10-11 16:07:56

标签: php unit-testing symfony symfony-forms

注意: 这是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表单的单元测试是集成测试,他们应该确保变压器不会失败。甚至这么说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的单元测试类中的所有模拟和期望现在都需要在这里重复。我对此不好,所以我的选择是什么?

  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 */
    }
    

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

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

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

1 个答案:

答案 0 :(得分:12)

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

它是这样的:

public boolean isInternetWorking() {
boolean success = false;
try {
    URL url = new URL("https://google.com");
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setConnectTimeout(10000);
    connection.connect();
    success = connection.getResponseCode() == 200;
} catch (IOException e) {
    e.printStackTrace();
}
return success;
}

现在要进行测试,您需要创建一个扩展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; } } 显然是你需要它的地方。

两个最突出的优点是没有涉及工厂,因此复杂性仍然被封装,并且这种变化几乎不可能破坏现有代码。

缺点是这种技术需要额外的课程。更重要的是,它需要一个知道被测试类内部的类。

为了避免额外的类,或者更确切地说隐藏额外的类,可以将其封装在一个函数中,而是创建一个匿名类(在PHP 7中添加了对匿名类的支持)。

$this->mock