在PHPUnit提供程序中使用工厂失败

时间:2017-07-17 08:57:07

标签: php laravel phpunit laravel-5.3

我正在尝试使用模型工厂在数据提供程序中创建模型。如果我在安装方法或测试中直接使用工厂,它可以工作,但如果我尝试在数据提供程序中使用它,我会收到错误:

  

1)警告

     

为MyClassTest :: testSomeMethod指定的数据提供程序无效。

     

无法找到名称为[默认] [App \ Model \ User]的工厂。

工厂定义:

/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(\App\Model\User::class, function (Faker\Generator $faker) {
    static $password;

    return [
        'id' => $faker->unique()->numberBetween(1, 10000),
        'email_address' => $faker->unique()->safeEmail,
        'remember_token' => str_random(10),
        'password' => $password ?: $password = bcrypt('secret'),
    ];
});

$factory->state(\App\Model\User::class, 'guest', function ($faker) {
    return [
        'role_id' => 9999,
    ];
});

我打电话给工厂:

factory(\App\Model\User::class)->states('guest')->make();

这是来自Laravel的错误还是我错过了什么?

修改

经过一些调试后,我发现在数据提供程序调用之前没有加载工厂定义,它们在调用setUp()方法时调用(即定义) - 这在数据提供程序调用之后发生 - 因此它不能在数据提供商处找到工厂。

所以在我看来,在数据提供者(或测试类中的任何静态方法)中使用工厂是不可能的。 或者我应该做些什么来在我的数据提供者方法中定义工厂!

3 个答案:

答案 0 :(得分:5)

我在另一个问题中找到答案(由同一问题引起)

因此,可以通过根据this answer在数据提供程序方法中调用$this->createApplication();$this->refreshApplication();来解决此问题, 或者根据this answer

在构造函数中调用它

代码看起来像这样

public function dataProvider() {
    $this->refreshApplication(); // or $this->createApplication();
    $user = factory(\App\Model\User::class)->states('guest')->make();

    // ...
    // another code to generate data ....
    // ...

    return $someFakeData;
}

我尝试过并且工作过,虽然我觉得它是一种解决方法,而不是事情应该如何运作,但任何“更清洁”的建议都会受到赞赏。

答案 1 :(得分:4)

有可能在数据提供者内部使用工厂,并且仍然使数据库事务正常工作!

今天这让我很沮丧,我想出了一个受this answer启发的解决方案,由于this answer

虽然不漂亮,但是在这里:

更新,我也将其转变为一篇博客文章,其内容更加详细:https://technicallyfletch.com/how-to-use-laravel-factories-inside-a-data-provider/

首先,我修改了使用提供程序的方式。我没有像通常那样期望一个参数列表,而是期望一个可以从中解构参数的函数。这才将工厂的执行推迟到我进入测试用例之后。

    /**
     * @test
     * @dataProvider validationProvider
     */
    public function it_validates_payload($getData)
    {
        // data provider now returns a function which we can invoke to
        // destructure our arguments here.
        [$ruleName, $payload] = $getData();

        $this->post(route('participants.store'), $payload)
            ->assertSessionHasErrors($ruleName);
    }

我的提供者现在变成这样:

    public function validationProvider()
    {
        return [
            'it fails if participant_type_id does not exist' => [
                function () {
                    return [
                        'participant_type_id',
                        array_merge($this->getValidData(), ['participant_type_id' => null])
                    ];
                }
            ],
            'it fails if first_name does not exist' => [
                function () {
                    return [
                        'first_name',
                        array_merge($this->getValidData(), ['first_name' => null])
                    ];
                }
            ],
            'it fails if last_name does not exist' => [
                function () {
                    return [
                        'last_name',
                        array_merge($this->getValidData(), ['last_name' => null])
                    ];
                }
            ],
            'it fails if email is not unique' => [
                function () {
                    $existingEmail = factory(Participant::class)->create([
                        'email' => 'pbeasly@dundermifflin.com'
                    ])->email;
                    return [
                        'email',
                        array_merge($this->getValidData(), ['email' => $existingEmail])
                    ];
                }
            ],
        ];
    }

然后这是重点,但它很好地说明了您可以推迟工厂。 getValidData()方法仅返回有效数据的数组,因此每个测试一次仅声明一个验证错误。它也使用工厂:

    private function getValidData()
    {
        return [
            'practice_id' => factory(Practice::class)->create()->id,
            'participant_type_id' => 1,
            'first_name' => 'John',
            'last_name' => 'Doe',
        ];
    }

有些事情仍然困扰着我

  1. 看起来很草率。数据提供者已经很难使其可读,这只会使它变得更糟。尽管您可以抽象一个实用程序来帮助清理此问题。
  2. 在我的示例中,我有一个针对每种情况进行的数据库调用,因为该调用随提供程序返回的闭包的每次执行而运行。我不确定最好的解决方法是什么,但是一种方法是在类本身的构造函数中设置一些状态。在第一次执行提供程序后创建id后,您可以从状态中提取ID,而不必每次都对数据库进行新的调用。

否则,它是当您需要此功能时的可行解决方案,希望当其他人偶然发现此功能时,它会对您有所帮助!

答案 2 :(得分:0)

待办事项

dd(\App\Model\User::class);

要查看它是否返回完全合格的班级名称,如果它没有,则可能是您的问题。