xi / fixtures
在测试中方便创建 Doctrine 实体。类似于 Ruby 的 FactoryGirl。
Requires
- php: >=5.3.0
- doctrine/orm: 2.*
Requires (Dev)
- phpunit/phpunit: 3.7.*
This package is not auto-updated.
Last update: 2024-09-23 11:11:36 UTC
README
Xi Fixtures 提供在测试中方便且可扩展地创建 Doctrine 实体的功能。如果您熟悉 Ruby 的 FactoryGirl,那么这基本上是 Doctrine/PHP 的相同功能。
简而言之
想象一下,我们正在设置一个测试,需要在数据库中设置 3 个用户。使用 Xi Fixtures,我们可以在一个地方指定每个用户需要一个唯一的用户名,并且需要属于一个组(通过一对多关系)
$this->factory ->define('User') ->sequence('username', 'user_%d') ->field('administrator', false) ->reference('group', 'Group');
现在在我们的测试中,我们可以简单地这样表示
$user1 = $this->factory->get('User'); $user2 = $this->factory->get('User'); // We can selectively override attributes $user3 = $this->factory->get('User', array('administrator' => true)); testStuffWith($user1, $user2, $user3);
动机
许多网络应用程序具有非平凡的数据库结构,表之间有很多依赖关系。此类应用程序的一个组件可能只处理一个或两个表中的实体,但这些实体可能需要复杂的实体图才能有用或通过验证。
例如,一个 User
可能是 Group
的成员,而 Group
又是 Organization
的一部分,反过来,Organization
又依赖于五个不同的表,这些表描述了关于该组织的各种信息。您正在编写一个组件,用于更改用户的密码,并且目前对组、组织及其依赖不感兴趣。您如何设置测试?
- 您是否在
setUp()
中创建Organization
和Group
的所有依赖关系以获取一个有效的User
?不,那将是极其繁琐和冗长的。 - 您是否为所有测试创建一个包含示例组织的共享固定文件,其中包含满足依赖关系?不,有大量测试依赖于单个固定文件,在以后更改该固定文件时会使操作变得困难。
- 您是否使用模拟对象?当然,但在许多情况下,您正在测试的代码与实体之间的交互方式非常复杂,以至于模拟它们是不切实际的。
Xi Fixtures 是在 (1) 和 (2) 之间的折中方案。您在一个中心位置指定如何生成实体及其依赖关系,但在测试中显式创建它们,仅覆盖您想要修改的字段。
教程
我们将假设您有一个用于测试的基类,该基类设置了一个连接到最小初始化的空测试数据库的新鲜 EntityManager
。一个简单的工厂设置看起来像这样。
<?php use Xi\Fixtures\FixtureFactory; use Xi\Fixtures\FieldDef; abstract class TestCase extends \PHPUnit_Framework_TestCase { protected $factory; public function setUp() { // ... (set up a blank database and $this->entityManager) ... $this->factory = new FixtureFactory($this->entityManager); $this->factory->setEntityNamespace('What\Ever'); // If applicable // Define that users have names like user_1, user_2, etc., // that they are not administrators by default and // that they point to a Group entity. $this->factory ->define('User') ->sequence('username', 'user_%d') ->field('administrator', false) ->reference('group', 'Group'); // Define a Group to just have a unique name as above. // The order of the definitions does not matter. $this->factory ->define('Group') ->sequence('name', 'group_%d'); // If you want your created entities to be saved by default // then do the following. You can selectively re-enable or disable // this behavior in each test as well. // It's recommended to only enable this in tests that need it. // In any case, you'll need to call flush() yourself. $this->factory->persistOnGet(); } }
现在您可以轻松地获取实体并覆盖与您的测试案例相关的字段,如下所示。
<?php class UserServiceTest extends TestCase { // ... public function testChangingPasswords() { $user = $this->factory->get('User', array( 'name' => 'John' )); $this->service->changePassword($user, 'xoo'); $this->assertSame($user, $this->service->authenticateUser('john', 'xoo')); } }
单例
有时您的实体具有具有对某些实体类型多个引用的依赖图。例如,应用程序可能有一个“当前组织”的概念,用户、组、产品、类别等属于一个组织。默认情况下,FixtureFactory
会为每个需要的 Organization
创建一个新的 Organization
,但这并不总是您想要的。有时您希望每个新实体都指向一个共享的 Organization
。
您的第一个反应应该是避免此类情况,并在可能的情况下明确指定共享实体。如果由于某种原因不可行,则 FixtureFactory
允许您将实体制作成 单例。如果存在特定类型的实体单例,则 get()
将返回该单例而不是创建新实例。
<?php class SomeTest extends TestCase { public function setUp() { parent::setUp(); $this->org = $this->factory->getAsSingleton('Organization'); } public function testSomething() { $user1 = $this->factory->get('User'); $user2 = $this->factory->get('User'); // now $user1->getOrganization() === $user2->getOrganization() ... } }
强烈建议仅在单个测试类的设置中创建单例,而不是在测试的基类中。
多对多
FixtureFactory 帮助您开始构建多对多关联。
以下示例创建了一个属于三个组的用户。关联的两侧都会更新。
<?php $factory ->define('User') ->referenceMany('group', 'Group', 'users', 3); // 'group' is the field in User // 'Group' is the target entity // 'users' is the inverse field in 'Group' // 3 is the default number of 'Group' entities to generate. $user = $factory->get('User');
上述代码在关联为一对多的情况下同样有效。这是从 'many' 方面使用 ->reference()
的替代方法。
高级
您可以为实体创建后设置其字段之后调用的 afterCreate
回调。在这里,您可以调用实体的构造函数。由于 Doctrine 也没有默认调用构造函数,所以 FixtureFactory
不会这样做。
<?php $factory->define('User') ->sequence('username', 'user_%d') ->afterCreate(function(User $user, array $fieldValues) { $user->__construct($fieldValues['username']); });
您可以使用 entityType
方法在不同的名称下定义同一实体的多个版本。
<?php $factory->define('NormalUser') ->entityType('User') ->sequence('username', 'user_%d') ->field('administrator', false); $factory->define('Administrator') ->entityType('User') ->sequence('username', 'user_%d') ->field('administrator', true);
API 参考
<?php // Defining entities $factory->define('EntityName') ->field('simpleField', 'constantValue') ->field('generatedField', function($factory) { return ...; }) ->sequence('sequenceField1', 'name-%d') // name-1, name-2, ... ->sequence('sequenceField2', 'name-') // the same ->sequence('sequenceField3', function($n) { return "name-$n"; }) ->reference('referenceField', 'OtherEntity') ->referenceMany('referenceField', 'OtherEntity', 'inverseField', $count) ->afterCreate(function($entity, $fieldValues) { // ... }) ->entityType('Type') // or '\Namespaced\Type' // Getting an entity (new or singleton) $factory->get('EntityName', array('field' => 'value')); // If you have set persistOnGet to true and still want an unpersisted Entity $factory->getUnpersisted('EntityName', array('field' => 'value')); // Singletons $factory->getAsSingleton('EntityName', array('field' => 'value')); $factory->setSingleton('EntityName', $entity); $factory->unsetSingleton('EntityName'); // Configuration $this->factory->setEntityNamespace('What\Ever'); // Default: empty $this->factory->persistOnGet(); // Default: don't persist $this->factory->persistOnGet(false);
杂项
FixtureFactory
和DSL
被设计成可子类化。- 对于双向的一对多关联,只要您记得在您的映射中指定
inversedBy
属性,'one' 方面的集合就会更新。 - 如果您在测试之间共享 Doctrine 实体管理器,请记住在测试之间使用
$em->clear()
清除其内部状态。
变更日志
-
1.1.1
- 添加了
referenceMany
,并使一对多引用在多端可指定。
- 添加了
-
1.1
- 弃用了旧版 API,实现了 DSL。
-
1.0
- 首次发布,带有旧版 API。