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

PHPUnit: How do I mock an EntityManager without mocking the Metadata?

使用 PHPUnit 和 Doctrine 我经常最终编写非常大的方法来模拟 Doctrine ClassMetadata,尽管在??我看来它不需要被模拟,因为它可以被视为稳定的。我仍然需要模拟 EntityManager 因为我不希望 Doctrine 连接到数据库。

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

我正在将 DoctrineModule 用于 Zend 2,因此能够使用我的配置来获取 Metadata 对象会很有用,但我认为手动读取所需部分也可以.

示例:

1
2
3
4
5
6
7
8
9
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');
}

以及实际类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
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 视为一个稳定的组件 - 不必要的工作。


在花了很多时间研究 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 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * @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 方法中设置任何对象来操作或替换任何对象,然后使用 getEmMock() 获取所需的 EntityManager 模拟。

有点大,但这里是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
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 解析器中抛出异常,因为这个解析器不使用自动加载并且只检查已经加载的类。所以如果你仍然想使用它们,你只需要在解析类注释之前手动包含它们。