Zend framework2 PHPUnit:如何在不模拟元数据的情况下模拟EntityManager?

Zend framework2 PHPUnit:如何在不模拟元数据的情况下模拟EntityManager?,zend-framework2,doctrine,phpunit,Zend Framework2,Doctrine,Phpunit,在使用PHPUnit和Doctrine时,我经常会编写非常大的方法来模拟DoctrinesClassMetadata,尽管我认为它不需要被模拟,因为它可以被视为是稳定的。我仍然需要模拟EntityManager,因为我不想连接到数据库 因此,我的问题是:如何在不需要数据库连接的情况下通过EntityManagermock获取ClassMetadata?对于所有最终的数据库调用,EntityManager仍然需要是一个mock,我只是不想再次写下所有的元数据 我正在为Zend 2使用doctriM

在使用PHPUnit和Doctrine时,我经常会编写非常大的方法来模拟Doctrines
ClassMetadata
,尽管我认为它不需要被模拟,因为它可以被视为是稳定的。我仍然需要模拟
EntityManager
,因为我不想连接到数据库

因此,我的问题是:如何在不需要数据库连接的情况下通过
EntityManager
mock获取
ClassMetadata
?对于所有最终的数据库调用,
EntityManager
仍然需要是一个mock,我只是不想再次写下所有的元数据

我正在为Zend 2使用
doctriMemodule
,因此能够使用我的配置来获取
元数据
对象是很有用的,但是我假设手动读取所需的部分也是可以的

示例:

public function testGetUniqueFields()
{
    $this->prepareGetUniqueFields(); // about 50 lines of mocking ClassMetadata
    $entity = 'UniqueWithoutAssociation';
    $unique = $this->handler->getUniqueFields($entity);
    $expected = ["uniqueColumn"];
    $this->assertEquals($expected, $unique,
        'getUniqueFields does not return the unique fields');
}
以及实际类别的代码:

public function getUniqueFields($class)
{
    $unique = array();
    $metadata = $this->getClassMetadata($class);
    $fields = $metadata->getFieldNames();
    foreach ($fields as $field) {
        if($metadata->isUniqueField($field) && !$metadata->isIdentifier($field)) {
            $unique[] = $field;
        }
    }
    return $unique;
}

测试工作与预期的一样,但每次我测试另一个方法或该方法的另一个行为时,我都需要再次准备模拟或结合过去的定义。另外,这段代码所需的50行代码是本测试中最少的一行代码。大多数测试类都是关于
ClassMetadata
mock的。这是一项耗时的工作,如果您将
ClassMetadata
视为一个稳定的组件,那么这是一项不必要的工作。

在花了很多时间研究信条源代码之后,我找到了一个解决方案

同样,只有当您经常使用doctrins
ClassMetadata
对象,以至于模仿每个方法调用变得不干净时,才可以使用此解决方案。在其他情况下,您仍然应该创建一个
类元数据的模拟

尽管如此,由于Composer的最小稳定性设置被设置为稳定,这样的组件可以被视为稳定的,因此不需要创建模拟对象

ClassMetadata
依赖于其他几个
原则
类,这些类都是通过无处不在的
EntityManager
注入的:

  • 条令\ORM\Configuration
    获取实体路径

    • 条令\Common\Annotations\AnnotationReader
      条令\ORM\Mapping\Driver\AnnotationDriver
      通过
      配置
      对象注入
  • Doctrine\DBAL\Connection
    获取数据库平台以了解标识符策略。应模拟此对象,以便不可能进行数据库调用

    • 条令\DBAL\Platforms\AbstractPlatform
      如前所述
  • 原则\Common\EventManager
    触发某些事件

对于单个测试方法或简单方法调用,我创建了一个返回
EntityManager
mock对象的方法,该对象能够返回有效的
ClassMetadata
对象:

/**
 * @return EntityManager|\PHPUnit_Framework_MockObject_MockObject
 */
public function getEmMock()
{
    $dir = __DIR__."/Asset/Entity/";
    $config = Setup::createAnnotationMetadataConfiguration(array($dir), true);
    $eventManager = new \Doctrine\Common\EventManager();
    $platform = new PostgreSqlPlatform();
    $metadataFactory = new ClassMetadataFactory();
    $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader()));

    $connectionMock = $this->getMockBuilder('Doctrine\DBAL\Connection')
        ->disableOriginalConstructor()
        ->getMock();
    $connectionMock->expects($this->any())
        ->method('getDatabasePlatform')
        ->will($this->returnValue($platform));

    /** @var EntityManager|\PHPUnit_Framework_MockObject_MockObject $emMock */
    $emMock = $this->getMockBuilder('Doctrine\ORM\EntityManager')
        ->disableOriginalConstructor()
        ->getMock();
    $metadataFactory->setEntityManager($emMock);
    $emMock->expects($this->any())
        ->method('getConfiguration')
        ->will($this->returnValue($config));
    $emMock->expects($this->any())
        ->method('getConnection')
        ->will($this->returnValue($connectionMock));
    $emMock->expects($this->any())
        ->method('getEventManager')
        ->will($this->returnValue($eventManager));
    $emMock->expects($this->any())
        ->method('getClassMetadata')
        ->will($this->returnCallback(function($class) use ($metadataFactory){
            return $metadataFactory->getMetadataFor($class);
        }));
    return $emMock;
}
在这里,您甚至可以通过调用为
EntityManager
mock创建的getter来操作所有对象。但这并不完全是干净的,而且在某些情况下,这种方法仍然是不灵活的。这仍然是一个简单的解决方案,例如,您可以添加一些参数,并将该方法置于trait中以重用它

为了进一步的需要,我创建了一个抽象类,它提供了最大的灵活性,并允许您模拟其他所有内容或以其他方式创建一些组件

它需要两种配置:实体路径和平台对象。您可以通过在
setUp
方法中设置对象来操作或替换任何对象,然后使用
getemock()
获取所需的
EntityManager
mock

稍微大一点,但在这里:

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Tools\Setup;

/**
 * Class AbstractTestWithMetadata
 * @author Marius Teller
 */
abstract class AbstractTestWithMetadata extends \PHPUnit_Framework_TestCase
{

    const EXCEPTION_NO_ENTITY_PATHS_SET = "At least one entity path must be set";

    const EXCEPTION_NO_PLATFORM_SET = "An instance of Doctrine\\DBAL\\Platforms\\AbstractPlatform must be set";

    /**
     * @var array
     */
    protected $entityPaths = [];
    /**
     * @var AbstractPlatform
     */
    protected $platform;
    /**
     * @var EntityManager
     */
    protected $emMock;
    /**
     * @var Connection
     */
    protected $connectionMock;
    /**
     * @var Configuration
     */
    protected $configuration;
    /**
     * @var EventManager
     */
    protected $eventManager;
    /**
     * @var ClassMetadataFactory
     */
    protected $classMetadataFactory;


    /**
     * @return array
     * @throws \Exception
     */
    public function getEntityPaths()
    {
        if($this->entityPaths === []) {
            throw new \Exception(self::EXCEPTION_NO_ENTITY_PATHS_SET);
        }
        return $this->entityPaths;
    }

    /**
     * @param array $entityPaths
     */
    public function setEntityPaths(array $entityPaths)
    {
        $this->entityPaths = $entityPaths;
    }

    /**
     * add an entity path
     * @param string $path
     */
    public function addEntityPath($path)
    {
        $this->entityPaths[] = $path;
    }

    /**
     * @return AbstractPlatform
     * @throws \Exception
     */
    public function getPlatform()
    {
        if(!isset($this->platform)) {
            throw new \Exception(self::EXCEPTION_NO_PLATFORM_SET);
        }
        return $this->platform;
    }

    /**
     * @param AbstractPlatform $platform
     */
    public function setPlatform(AbstractPlatform $platform)
    {
        $this->platform = $platform;
    }

    /**
     * @return EntityManager
     */
    public function getEmMock()
    {
        if(!isset($this->emMock)) {
            /** @var EntityManager|\PHPUnit_Framework_MockObject_MockObject $emMock */
            $emMock = $this->getMockBuilder('Doctrine\ORM\EntityManager')
                ->disableOriginalConstructor()
                ->getMock();

            $config = $this->getConfiguration();
            $connectionMock = $this->getConnectionMock();
            $eventManager = $this->getEventManager();
            $classMetadataFactory = $this->getClassMetadataFactory();
            $classMetadataFactory->setEntityManager($emMock);

            $emMock->expects($this->any())
                ->method('getConfiguration')
                ->will($this->returnValue($config));
            $emMock->expects($this->any())
                ->method('getConnection')
                ->will($this->returnValue($connectionMock));
            $emMock->expects($this->any())
                ->method('getEventManager')
                ->will($this->returnValue($eventManager));
            $emMock->expects($this->any())
                ->method('getClassMetadata')
                ->will($this->returnCallback(function($class) use ($classMetadataFactory){
                    return $classMetadataFactory->getMetadataFor($class);
                }));
            $this->setEmMock($emMock);
        }
        return $this->emMock;
    }

    /**
     * @param EntityManager $emMock
     */
    public function setEmMock($emMock)
    {
        $this->emMock = $emMock;
    }

    /**
     * @return Connection
     */
    public function getConnectionMock()
    {
        if(!isset($this->connectionMock)) {
            $platform = $this->getPlatform();
            /** @var Connection|\PHPUnit_Framework_MockObject_MockObject $connectionMock */
            $connectionMock = $this->getMockBuilder('Doctrine\DBAL\Connection')
                ->disableOriginalConstructor()
                ->getMock();
            $connectionMock->expects($this->any())
                ->method('getDatabasePlatform')
                ->will($this->returnValue($platform));
            $this->setConnectionMock($connectionMock);
        }
        return $this->connectionMock;
    }

    /**
     * @param Connection $connectionMock
     */
    public function setConnectionMock($connectionMock)
    {
        $this->connectionMock = $connectionMock;
    }

    /**
     * @return Configuration
     */
    public function getConfiguration()
    {
        if(!isset($this->configuration)) {
            $config = Setup::createAnnotationMetadataConfiguration($this->getEntityPaths(), true);
            $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader()));
            $this->setConfiguration($config);
        }
        return $this->configuration;
    }

    /**
     * @param Configuration $configuration
     */
    public function setConfiguration(Configuration $configuration)
    {
        $this->configuration = $configuration;
    }

    /**
     * @return EventManager
     */
    public function getEventManager()
    {
        if(!isset($this->eventManager)) {
            $this->setEventManager(new EventManager());
        }
        return $this->eventManager;
    }

    /**
     * @param EventManager $eventManager
     */
    public function setEventManager($eventManager)
    {
        $this->eventManager = $eventManager;
    }

    /**
     * @return ClassMetadataFactory
     */
    public function getClassMetadataFactory()
    {
        if(!isset($this->classMetadataFactory)) {
            $this->setClassMetadataFactory(new ClassMetadataFactory());
        }
        return $this->classMetadataFactory;
    }

    /**
     * @param ClassMetadataFactory $classMetadataFactory
     */
    public function setClassMetadataFactory(ClassMetadataFactory $classMetadataFactory)
    {
        $this->classMetadataFactory = $classMetadataFactory;
    }
}

还有一个提示:其他类的注释可能有问题,例如
Zend\Form\Annotation\Validator
。这样的注释将在Doctrines解析器中引发异常,因为该解析器不使用自动加载,只检查已加载的类。因此,如果仍要使用它们,只需在解析类注释之前手动包含它们。

为什么需要mocking EntityManager?这样,您可能会结束测试原则,而不是您的代码,这不是真正有用的。你能提供你想测试的代码吗?有一些更好的测试方法,例如存储库。如前所述,我认为
ClassMetadata
是一个稳定的组件,我可以在不测试它的情况下使用它,就像在测试中使用Zend的临时实例而不模拟它们一样(参见Zend 2的PHPUnit教程)。这不是关于存储库之类的类,而是
doctrimemodule
等的扩展。在这些扩展中,我需要读取大量元数据,以便生成表单的输入筛选器。我知道,我会使用
ClassMetadata
。这没什么错。始终需要考虑上下文。我遵循一条经验法则:如果你在模仿3个以上的方法(大约),重新考虑设计(如果可以)或直接使用类。说起来容易做起来难:-(我花了几个小时试图得到一个干净的
ClassMetadata
实例,而没有创建
EntityManager
,我明白了。请把你想测试的最小代码添加到一些要点中,好吗?我会看看的。