xi/fixtures

在测试中方便创建 Doctrine 实体。类似于 Ruby 的 FactoryGirl。

1.1.1 2013-05-13 06:05 UTC

This package is not auto-updated.

Last update: 2024-09-23 11:11:36 UTC


README

Xi Fixtures 提供在测试中方便且可扩展地创建 Doctrine 实体的功能。如果您熟悉 Ruby 的 FactoryGirl,那么这基本上是 Doctrine/PHP 的相同功能。

Build Status

简而言之

想象一下,我们正在设置一个测试,需要在数据库中设置 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 又依赖于五个不同的表,这些表描述了关于该组织的各种信息。您正在编写一个组件,用于更改用户的密码,并且目前对组、组织及其依赖不感兴趣。您如何设置测试?

  1. 您是否在 setUp() 中创建 OrganizationGroup 的所有依赖关系以获取一个有效的 User?不,那将是极其繁琐和冗长的。
  2. 您是否为所有测试创建一个包含示例组织的共享固定文件,其中包含满足依赖关系?不,有大量测试依赖于单个固定文件,在以后更改该固定文件时会使操作变得困难。
  3. 您是否使用模拟对象?当然,但在许多情况下,您正在测试的代码与实体之间的交互方式非常复杂,以至于模拟它们是不切实际的。

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);

杂项

  • FixtureFactoryDSL 被设计成可子类化。
  • 对于双向的一对多关联,只要您记得在您的映射中指定 inversedBy 属性,'one' 方面的集合就会更新。
  • 如果您在测试之间共享 Doctrine 实体管理器,请记住在测试之间使用 $em->clear() 清除其内部状态。

变更日志

  • 1.1.1

    • 添加了 referenceMany,并使一对多引用在多端可指定。
  • 1.1

    • 弃用了旧版 API,实现了 DSL。
  • 1.0

    • 首次发布,带有旧版 API。