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

标签 php unit-testing symfony symfony-forms

注意:这是 Symfony < 2.6,但我相信无论版本如何,同样的总体问题都适用

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

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';
    }
}

这行得通,很简单,而且大部分看起来都像您看到的将数据转换器添加到表单类型的所有示例。直到你进行单元测试。看到问题了吗?变形金刚不能被 mock 。 “可是等等!”你说,“Symfony 表单的单元测试是集成测试,它们应该确保转换器不会失败。甚至这么说 in the documentation!”

This test checks that none of your data transformers used by the form failed. The isSynchronized() method is only set to false if a data transformer throws an exception

好吧,那么你就接受了你无法隔离变压器的事实。没什么大不了?

现在考虑对具有此类型字段的表单进行单元测试时会发生什么(假设 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. 将转换器作为选项之一注入(inject)

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

  2. 在表单类型中注入(inject)一个转换工厂

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

    在我考虑工厂的实际外观之前,这感觉还不错。对于初学者来说,它需要注入(inject)实体管理器。但那又怎样?如果我进一步看下去,这个所谓的通用工厂可能需要各种依赖项来创建不同类型的数据转换器。这显然不是一个好的长期设计决策。那又怎样呢?将其重新标记为 EntityManagerAwareDataTransformerFactory?这里开始变得凌乱。

  3. 我没有想到的事情......

想法?经验?可靠的建议?

最佳答案

首先,我几乎没有使用 Symfony 的经验。但是,我认为您在那里错过了第三种选择。在 Working Effectively with Legacy Code 中,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 显然是您需要的任何内容。

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

缺点是这种技术需要额外的类(class)。更重要的是,它需要一个了解被测类内部结构的类。


为了避免额外的类,或者更确切地说隐藏额外的类,可以将其封装在一个函数中,创建一个匿名类(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();
        /* ... */
    }

}

关于php - 如何使用 Symfony 表单和数据转换器实现测试隔离?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39981800/

相关文章:

java - Mockito 静态函数模拟

php - 在 Laravel 中对闭包进行单元测试

symfony - symfony2 中的 containerAware 和 Controller

php - Symfony2 : FOSUserBundle - Single firewall, 多个登录表单/入口点

php - 在 PHP/MySQL 中只获取一行

javascript - 防止以下系统中的手动 POST 请求

php - 将Js中的Regexp转换成PHP?

c++ - 单元测试资源管理类中的私有(private)方法 (C++)

javascript - 使用 getJSon 函数将 JSON 数据解析为 html 页面

php - 将多个 $_Posts 从表单添加到 MYSQL DB - 使用循环?