Laravel 8 factory state from a unit test data provider? (porting from Laravel 7)

Viewed 265

In Laravel 7 we pretty routinely use the pattern:

public function provideCanDoThing(): array
{
    return [
        "Root can do thing" => [['root'], true],
        "Normal user cannot do thing" => [[], false],
        "Root cannot do thing while suspended" => [['root', 'suspended'], false],
    ];
}

/**
  * @dataProvider provideCanDoThing
  */
public function testCanDoThing(array $userStates, bool $expectedCanDo): void
{
    $user = factory(User::class)->states($userStates)->create();
    self::expectSame($expectedCanDo, $user->canDoThing());
}

The state "root" can be pretty complicated, with a combo of attributes and afterCreating logic.

In new Laravel 8 factories, the state method no longer runs the collected changes in a named state, instead it... works just like passing attributes to make or create?

I can see how I could rewrite the test more like

$userFactory = User::factory();
foreach($userStates as $userState){
    $userFactory = $userFactory->{$userState}();
}
$user = $userFactory->create();

But that seems much less expressive. I guess I'm surprised I can't find a method on Factory that does that for me.

1 Answers

Seems like solving this problem will be highly demanded when more people will migrate their factories and tests to Laravel 8.

I wrote this solution within a trait that can be used by a factory:

<?php

namespace App\Traits;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Arr;

trait FactoryApplyStates
{
    /**
     * Applies one or more states to a model factory
     *
     * @param string|array $stateMethods
     * @return Factory
     * @throws \Exception
     */
    public function applyStates($stateMethods = null)
    {
        $stateMethods = Arr::wrap($stateMethods);

        $factory = $this;
        /* @var Factory $factory */

        foreach ($stateMethods as $stateMethod) {
            if (is_callable($stateMethod)) {
                $factory = $stateMethod($factory);
            } else {
                if (!is_string($stateMethod)) {
                    throw new \Exception('Incorrect state name: must be a name of method');
                }
                if (!method_exists($this, $stateMethod)) {
                    throw new \Exception('Unsupported state for ' . __CLASS__ . ': ' . $stateMethod . '() method does not exist');
                }
                $factory = $factory->{$stateMethod}();
            }
        }

        return $factory;
    }
}

And also started a discussion at Laravel's Github repo to see how many people are interested in this way of using states at their tests: https://github.com/laravel/framework/discussions/40710

Related