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

问题描述 投票:0回答:5

我正在尝试使用模型工厂在数据提供程序中制作模型。如果我在设置方法或直接测试中使用工厂,它会起作用,但如果我尝试在数据提供程序中使用它,则会收到错误:

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() 方法时被调用(即定义) - 这发生在数据提供程序调用之后 - 因此它找不到数据提供者中的工厂。

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

php laravel phpunit laravel-5.3
5个回答
17
投票

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

因此,这个问题可以通过根据

这个答案
在数据提供者方法中调用
$this->createApplication();
$this->refreshApplication();来解决, 或者根据 this 答案

在构造函数中调用它

代码看起来像这样

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

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

    return $someFakeData;
}

我尝试过并成功了,尽管我觉得这是一种解决方法,而不是事情应该如何运作,但任何“更干净”的建议将不胜感激。


14
投票

可以在数据提供者内部使用工厂并且仍然可以进行数据库事务!

今天这让我很沮丧,我找到了一个受这个答案启发的解决方案,这要归功于这个答案

它不漂亮,但它就是:

更新我还将其变成了一篇博客文章,其中更详细一些: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' => '[email protected]'
                    ])->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. 在我的示例中,我有一个针对每个场景进行的数据库调用,因为它随着提供者返回的闭包的每次执行而运行......yuk!我不确定解决这个问题的最佳方法是什么,但一种方法是在类本身的构造函数中设置某种状态。在首次执行提供程序后创建
    id
    后,您可以从状态中提取 id,而不是每次都对数据库进行新的调用。

否则,当您需要这个时,这是一个有效的解决方案,我希望它可以帮助其他人(如果他们碰巧找到这个)!


2
投票

或者,您可以使用Pest,其中工厂仅在数据提供商中工作。

it('can just use a factory in a data provider', function(YourModel $yourModel) {
    expect($yourModel)->toBeInstanceOf(YourModel::class);
})->with([
    [fn() => YourModel::factory()->create()],
]);

0
投票

@丹·弗莱彻

绝妙的解决方案(这个https://stackoverflow.com/a/61807494)!

您必须从数据提供商处调用

createApplication
的另一种解决方案对我来说不起作用(除了效率低下之外)。

我认为你可以通过使用“速记闭包”来使语法稍微更容易接受,可以选择与

yield
结合使用,例如

yield [ fn ($user) => $this->getData($user, 'subject', null, 'Subject is required.') ];

闭包的

$user
参数将是您通过 Laravel Factory 创建的模型对象(这就是最终的重点)。


-2
投票

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

查看它是否返回正确的完全限定类名,如果没有,那么这可能是您的问题。

© www.soinside.com 2019 - 2024. All rights reserved.