flyos/factory-girl-php

测试用例的固定替代品 - Thoughtbot的Ruby Factory Girl的PHP版本

dev-master 2016-12-19 16:05 UTC

This package is not auto-updated.

Last update: 2024-09-16 03:56:45 UTC


README

Build Status

A PHP port of Thoughtbot's Ruby Factory Girl. Based on a fork of breerly/factory-girl-php.

FactoryGirl FixtureFactory

FactoryGirl\Provider\Doctrine\FixtureFactory 提供在测试中方便创建Doctrine实体。如果您熟悉Ruby的 FactoryGirl,那么这对于Doctrine/PHP来说本质上是一样的。

动机

许多网络应用程序具有非平凡的数据库结构,表与表之间有许多依赖关系。此类应用程序的一个组件可能只处理一个或两个表中的实体,但这些实体可能依赖于复杂的实体图才有用或通过验证。

例如,一个 User 可能是 Group 的成员,而 Group 又是 Organization 的一部分,反过来又依赖于五个不同的表来描述关于该组织的各种信息。您正在编写一个更改用户密码的组件,目前对组、组织及其依赖不感兴趣。您如何设置您的测试?

  1. 您是否在 setUp() 中为 OrganizationGroup 创建所有依赖关系以在您的测试中获得一个有效的 User?不,那会非常繁琐和冗长。
  2. 您是否为所有测试创建一个共享的固定装置,其中包含一个具有满足依赖关系的示例组织?不,这将使固定装置变得非常脆弱。
  3. 您是否使用模拟对象?当然,在实用的情况下。然而,在许多情况下,您正在测试的代码与实体的交互方式非常复杂,以至于充分模拟它们是不切实际的。

FixtureFactory 是在 (1)(2) 之间的折中方案。您在一个中心位置指定如何生成您的实体及其依赖关系,但在您的测试中明确创建它们,仅覆盖您想要的字段。

教程

我们假设您有一个用于测试的基类,该类安排一个连接到最小初始化的空测试数据库的新鲜 EntityManager。一个简单的工厂设置如下。

<?php
use FactoryGirl\Provider\Doctrine\FixtureFactory,
    FactoryGirl\Provider\Doctrine\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->defineEntity('User', array(
            'username' => FieldDef::faker()->username,
            'administrator' => false,
            'group' => FieldDef::reference('Group')
        ));

        // Define a Group to just have a unique name as above.
        // The order of the definitions does not matter.
        $this->factory->defineEntity('Group', array(
            'name' => FieldDef::faker()->sentence()
        ));


        // 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

您的第一个反应应该是避免这种情况,并在可能的情况下明确指定共享实体。如果由于某种原因不可行,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() ...
    }
}

强烈建议仅在单个测试类的设置中创建单例,而不是在测试的基类中。

高级

您可以在实体创建并设置字段后调用一个 'afterCreate' 回调。在这里,您可以调用实体的构造函数,因为 FixtureFactory 默认不执行此操作。

<?php
$factory->defineEntity('User', array(
    'username' => FieldDef::faker()->username,
), array(
    'afterCreate' => function(User $user, array $fieldValues) {
        $user->__construct($fieldValues['username']);
    }
));

API参考

<?php

use AppBundle\Entity\EntityName;

// Defining entities
$factory->defineEntity(EntityName::class, array(
    'simpleField' => 'constantValue',

    'generatedField' => FieldDef::faker()->paragraphs(5, true),
    'dateField' => FieldDef::faker()->dateTime('-1 year'),
    'localizedField' => FieldDef::faker('fr_FR')->phoneNumber,

    'referenceField' => FieldDef::reference('OtherEntity')
), array(
    'afterCreate' => function($entity, $fieldValues) {
        // ...
    }
));

// Getting an entity (new or singleton)
$factory->get(EntityName::class, array('field' => 'value'));

// Getting an array of entities
$numberOfEntities = 15;
$factory->getList(EntityName::class, array('field' => 'value'), $numberOfEntities);

// Singletons
$factory->getAsSingleton(EntityName::class, array('field' => 'value'));
$factory->setSingleton(EntityName::class, $entity);
$factory->unsetSingleton(EntityName::class);

// Configuration
$this->factory->setEntityNamespace('What\Ever');  // Default: empty
$this->factory->persistOnGet();                   // Default: don't persist
$this->factory->persistOnGet(false);

杂项

  • FixtureFactoryFieldDef被设计为可继承的。
  • 在双向一对一关联中,只要你在映射中指定了inversedBy属性,'one'一侧的集合就会更新。

开发

测试

您需要全局安装PHPUnit

composer install --global phpunit/phpunit

必须使用以下命令安装composer包:

composer install --prefer-source