ramsey / uuid

:snowflake: A PHP library for generating universally unique identifiers (UUIDs).
https://uuid.ramsey.dev
MIT License
12.44k stars 500 forks source link

Mocking Uuid::uuid4() in laravel #147

Closed cmosguy closed 7 years ago

cmosguy commented 7 years ago

Hey @ramsey ,

I am trying to create a uuid that is predictable for a specific integration test where I am hitting a route in Laravel and I generate some thumbnails based on the created user uuid. I am struggling on figuring out how to create a Mockery object that will just return the Uuid::uuid4()->toString() method with a known fake value.

Here is what I have in my eloquent model in Larave User.phpl:

    /**
     * Boot the Uuid trait for the model.
     *
     * @return void
     */
    public static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            $model->uuid = Uuid::uuid4();
        });
    }

Then my controller is doing this when creating a user:

                $user = Member::create([
                    'email' => $providerUser->getEmail(),
                    'confirmed' => true,
                    'first_name' => $name[0],
                    'last_name' => $name[1]
                ]);

So my integration test is going something I want to just setup top in the test:

    public function setMockForAllThumbnailSizes()
    {

        $sizes = [40, 50, 80, 150];
        \Storage::shouldReceive('disk')->times(8)->with('s3')->andReturnSelf();

        foreach ($sizes as $size) {
            \File::shouldReceive('get')->once()->with("/tmp/tn-$size-bar-uuid-baz-now.jpeg")->andReturn('some file contents');

            \Storage::shouldReceive('put')->once()->withArgs([
                "images/photos/tn-$size-bar-uuid-baz-now.jpeg",
                'some file contents'
            ])->andReturn(true);

            \Storage::shouldReceive('setVisibility')->once()->withArgs([
                "images/photos/tn-$size-bar-uuid-baz-now.jpeg",
                'public'
            ])->andReturn(true);
        }
    }

You can see the tn-$size-uuid-baz-now.jpeg that is the name of thumnbail that is generated after I create the user. I want in my test to set the output of Uuid:uuid4() to uuid-baz so that my other tests will work when generating the names of the thumbnails for the user.

I know this is long winded question but I am struggling to figure out how to swap in and control the output.

Have you ever played with Carbon? Check this out they have a testing aid: http://carbon.nesbot.com/docs/#api-testing

There is a way to set the now() method:

$knownDate = Carbon::create(2001, 5, 21, 12);          // create testing date
Carbon::setTestNow($knownDate);                        // set the mock (of course this could be a real mock object)
echo Carbon::now();                                    // 2001-05-21 12:00:00

Is there an easy way to do this for Uuid? If not, it's ok just going on a tangent here which is a nice to have here.

cmosguy commented 7 years ago

@ramsey btw, I did read your responses here: https://github.com/ramsey/uuid/issues/23 They are all very confusing and I do not understand the correct approach here.

ramsey commented 7 years ago

Update: I'm not ignoring your question; I've been dealing with moving, the holidays, death in family, and lack of Internet connectivity over the past few weeks. I hope to address your question later this week. :-)

cmosguy commented 7 years ago

@ramsey thank you for taking the time to respond, I am sorry to hear about your loss. I hope you and your family had a great holiday. Take care.

ramsey commented 7 years ago

In #23, the OP couldn't mock Ramsey\Uuid\Uuid because the class was marked final. That restriction has since been removed, and the Uuid class may now be mocked. What you're running into is an issue mocking the return value of Uuid::uuid4(), since it's a static method being called from within a unit under test. Fortunately, there are several ways to go about this.

Here's an example I've whipped up and tested, so I know either of these approaches will work. Let me know if you have any questions or need clarification.

<?php
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidFactory;

class UuidMockTest extends TestCase
{
    public function testKnownUuidByMockingFactory()
    {
        // Create a Uuid object from a known UUID string
        $stringUuid = '253e0f90-8842-4731-91dd-0191816e6a28';
        $uuid = Uuid::fromString($stringUuid);

        // Partial mock of the factory;
        // returns $uuid object when uuid4() is called.
        $factoryMock = \Mockery::mock(UuidFactory::class . '[uuid4]', [
            'uuid4' => $uuid,
        ]);

        // Replace the default factory with our mock
        Uuid::setFactory($factoryMock);

        // Uuid::uuid4() is a proxy to the $factoryMock->uuid4() method, so
        // when Uuid::uuid4() is called, it calls the mocked method on the
        // factory and returns a valid Uuid object that we have defined.
        $this->assertSame($uuid, Uuid::uuid4());
        $this->assertEquals($stringUuid, Uuid::uuid4()->toString());
    }

    /**
     * @runInSeparateProcess
     * @preserveGlobalState disabled
     */
    public function testMockStaticUuidMethodToReturnKnownString()
    {
        // We replace the Ramsey\Uuid\Uuid class with one created by Mockery
        // (using the "alias:" prefix) so that we can mock the static uuid4()
        // method. For this to work without affecting the Ramsey\Uuid\Uuid
        // class used by other tests, we must run this in a separate process
        // with preserveGlobalState disabled (see method annotations).
        \Mockery::mock('alias:' . Uuid::class, [
            'uuid4' => 'uuid-baz',
        ]);

        // We've replaced Ramsey\Uuid\Uuid with our own class that defines the
        // method uuid4(). This method returns the string "uuid-baz."
        $this->assertEquals('uuid-baz', Uuid::uuid4());
    }
}
ramsey commented 7 years ago

@cmosguy Did my answer help?

cmosguy commented 7 years ago

@ramsey thanks for taking the time for writing a very clear test in your response.

This is super informative and helpful. I am now able to use this in my workflow. I had no clue you could trick phpunit like that @preserveGlobalState disabled and @runInSeparateProcess so that the mocking did not affect other tests, this is really useful. I hope this issue helps someone else.

Thanks for the super support!

trbsi commented 3 years ago

This is how I mocked it

use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Ramsey\Uuid\UuidFactory;

$factoryMock = $this->prophesize(UuidFactory::class);
$uuidInterface = $this->prophesize(UuidInterface::class);

$factoryMock->uuid4()->shouldBeCalled()->willReturn($uuidInterface->reveal());
$uuidInterface->toString()->shouldBeCalled()->willReturn('e36f227c-2946-11e8-b467-0ed5f89f718b');

// Replace the default factory with our mock
Uuid::setFactory($factoryMock->reveal());

This is how I called UUID generator in my class Uuid::uuid4()->toString()

j4r3kb commented 2 years ago

Also don't forget to do:

$defaultFactory = Uuid::getFactory();
...
Uuid::setFactory($defaultFactory);

after your tests are done so you don't mess up other tests/code that uses Uuid

agustingomes commented 2 years ago

The other day I encountered a similar problem in a project, because the default factory was overwritten inside Uuid.

For me, relying on the Uuid singleton inside the classes to generate UUID's instead of injecting the UuidFactory into the classes seems a bad practice because it couples very tightly the code to this library for instance, and you end up needing to work around the root cause of the issue by adopting the solution @j4r3kb pointed out above.

williamdes commented 2 years ago

This is how I did this thanks to https://github.com/baopham/laravel-dynamodb/issues/10#issuecomment-400094100 and https://github.com/ramsey/uuid/issues/147#issuecomment-977640310

use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidFactory;

        $defaultFactory = Uuid::getFactory();
        $requestId = $this->faker->uuid;

        // Partial mock of the factory
        // returns $uuid object when uuid4() is called.
        $factoryMock = \Mockery::mock(UuidFactory::class . '[uuid4]', [
            'uuid4' => Uuid::fromString($requestId),
        ]);

        Uuid::setFactory($factoryMock);
        // set back to default
        Uuid::setFactory($defaultFactory);
j4r3kb commented 1 year ago

One more approach if you have multiple calls to the Uuid factory in the tested code but only care about the next single/few of them:

use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidFactory;
use Ramsey\Uuid\UuidFactoryInterface;
use Ramsey\Uuid\UuidInterface;

class PrimedUuidFactory extends UuidFactory
{
    /**
     * @var array<int, UuidInterface>
     */
    private array $uuidList = [];

    public function __construct(
        private UuidFactoryInterface $originalFactory
    ) {
        parent::__construct();
    }

    public function uuid4(): UuidInterface
    {
        $uuid = array_shift($this->uuidList);
        if ($uuid === null) {
            return $this->originalFactory->uuid4();
        }

        return $uuid;
    }

    public function pushUuid(string $uuid4): void
    {
        $this->uuidList[] = Uuid::fromString($uuid4);
    }
}

Then in your test case:

        $uuidFactory = new PrimedUuidFactory(Uuid::getFactory());
        $uuidFactory->pushUuid('38b1020f-97eb-4561-8c95-7591caaebed3');
        Uuid::setFactory($this->uuidFactory);