breerly/factory-girl-php

针对关注和可读性测试的固定替换 - Thoughtbot's Ruby Factory Girl 的 PHP 版本

1.2.0 2020-03-07 13:37 UTC

This package is not auto-updated.

Last update: 2024-09-20 10:17:32 UTC


README

Continuous Integration

Thoughtbot 的 Ruby Factory Girl 的 PHP 版本。基于对 xi-doctrine 的分支。

FactoryGirl FixtureFactory

FactoryGirl\Provider\Doctrine\FixtureFactory 提供了在测试中方便地创建 Doctrine 实体的功能。如果你熟悉 Ruby 的 FactoryGirl,那么这基本上是 Doctrine/PHP 的相同功能。

动机

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

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

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

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

教程

我们假设你有一个为你的测试安排了一个全新的 EntityManager 的基础类,该 EntityManager 连接到一个最少初始化的空白测试数据库。一个简单的工厂设置看起来像这样。

<?php

use FactoryGirl\Provider\Doctrine\FieldDef;
use FactoryGirl\Provider\Doctrine\FixtureFactory;
use PHPUnit\Framework;

abstract class TestCase extends Framework\TestCase
{
    protected $factory;

    protected function setUp(): void
    {
        // ... (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', [
            'username' => FieldDef::sequence("user_%d"),
            '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', [
            'name' => FieldDef::sequence("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(): void
    {
        $user = $this->factory->get('User', [
            '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
{
    protected function setUp(): void
    {
        parent::setUp();
        $this->org = $this->factory->getAsSingleton('Organization');
    }

    public function testSomething(): void
    {
        $user1 = $this->factory->get('User');
        $user2 = $this->factory->get('User');

        // now $user1->getOrganization() === $user2->getOrganization() ...
    }
}

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

高级

您可以为创建实体后调用的事件指定一个'afterCreate'回调。在这里,例如,您可以调用实体的构造函数,因为FixtureFactory默认情况下不执行此操作。

<?php

$factory->defineEntity(
    'User', 
    [
        'username' => FieldDef::sequence("user_%d"),
    ], 
    [
    'afterCreate' => function(User $user, array $fieldValues) {
        $user->__construct($fieldValues['username']);
    }
]);

API参考

<?php

// Defining entities
$factory->defineEntity(
    'EntityName', 
    [
        'simpleField' => 'constantValue',
    
        'generatedField' => function($factory) { return ...; },
    
        'sequenceField1' => FieldDef::sequence('name-%d'), // name-1, name-2, ...
        'sequenceField2' => FieldDef::sequence('name-'),   // the same
        'sequenceField3' => FieldDef::sequence(function($n) { return "name-$n"; }),
    
        'referenceField' => FieldDef::reference('OtherEntity')
    ], 
    [
        'afterCreate' => function($entity, $fieldValues) {
            // ...
        }
    ]
);

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

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

// Singletons
$factory->getAsSingleton('EntityName', ['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);

杂项

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

开发

测试

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

composer install --prefer-source