Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mocking Uuid::uuid4() in laravel #147

Closed
cmosguy opened this issue Dec 12, 2016 · 11 comments
Closed

Mocking Uuid::uuid4() in laravel #147

cmosguy opened this issue Dec 12, 2016 · 11 comments
Labels

Comments

@cmosguy
Copy link

cmosguy commented Dec 12, 2016

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
Copy link
Author

cmosguy commented Dec 12, 2016

@ramsey btw, I did read your responses here: #23 They are all very confusing and I do not understand the correct approach here.

@ramsey
Copy link
Owner

ramsey commented Jan 3, 2017

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
Copy link
Author

cmosguy commented Jan 4, 2017

@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
Copy link
Owner

ramsey commented Jan 6, 2017

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
Copy link
Owner

ramsey commented Jan 9, 2017

@cmosguy Did my answer help?

@cmosguy
Copy link
Author

cmosguy commented Jan 9, 2017

@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
Copy link

trbsi commented Oct 8, 2020

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
Copy link

j4r3kb commented Nov 24, 2021

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
Copy link

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
Copy link

This is how I did this thanks to baopham/laravel-dynamodb#10 (comment) and #147 (comment)

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
Copy link

j4r3kb commented Jul 26, 2023

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);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants