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

时间:2015-10-12 12:37:17

标签: zend-framework2 doctrine phpunit

使用PHPUnit和Doctrine我经常最终编写非常大的方法来模拟Doctrines ClassMetadata,虽然在我看来它不需要被嘲笑,因为它可以看作是稳定的。我还是需要嘲笑EntityManager,因为我不想让Doctrine连接到数据库。

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

我使用DoctrineModule作为Zend 2,因此能够使用我的配置获取Metadata对象会很有用,但我认为它也可以手动阅读所需的部分。

示例:

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模拟的。这是一个耗时的工作 - 如果您将ClassMetadata视为一个稳定的组件 - 那就是不必要的工作。

1 个答案:

答案 0 :(得分:2)

在花了很多时间主演Doctrine源代码之后,我找到了一个解决方案。

再一次,这个解决方案只有在您经常使用Doctrines ClassMetadata对象时才会模拟每个方法调用变得不干净。在其他所有情况下,您仍应创建ClassMetadata的模拟。

尽管如此,由于作曲家的最小稳定性设置被设置为稳定,因此可以看出这些组件是稳定的,因此不需要创建模拟对象。

ClassMetadata取决于其他几个Doctrine类,这些类都是通过无处不在的EntityManager注入的:

  • Doctrine\ORM\Configuration获取实体路径

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

    • Doctrine\DBAL\Platforms\AbstractPlatform如上所述
  • Doctrine\Common\EventManager触发某些事件

对于单个测试方法或简单方法调用,我创建了一个方法,返回一个能够返回有效EntityManager对象的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模拟创建的getter来操纵所有对象。但这并不完全是干净的,并且在某些情况下该方法仍然不灵活。仍然是一个简单的解决方案,你可以添加一些参数并将该方法放入特征中以重复使用它。

为了满足更多需求,我创建了一个抽象类,它提供了最大的灵活性,允许您模拟其他所有内容或以其他方式创建一些组件。

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

稍大一些,但现在是:

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解析器中引发异常,因为此解析器不使用自动加载,只检查已加载的类。因此,如果您仍想使用它们,则只需在解析类注释之前手动包含它们。