Cam
Cam

Reputation: 71

Asserting a method call on a Facade not the underlying proxied class

I'm trying to write tests to assert that methods called in my AppServiceProvider boot method are present & enforced logic.

So far I'm using mocks & reflection, however I am having an issue with the DB::prohibitDestructiveCommands(app()->isProduction()) method call.

The method prohibitDestructiveCommands() isn't on the underlying proxied class (DatabaseManager), it's on the actual facade class.

This has made it quite difficult for me. I have spent far too long trying to solve this; to come up with a solution that will allow to assert that the facade method is called.

So I have landed on the following test as being a possible way to solve my issue:

test('Invokes DB::prohibitDestructiveCommands in production', function () 
    {
        $this->app->detectEnvironment(fn() => 'production');
        
        $mock = mock('overload:Illuminate\Support\Facades\DB');
    
        $mock->shouldReceive('getFacadeRoot')
             ->once()
             ->andReturn(new DB);
    
        $mock->shouldReceive('prohibitDestructiveCommands')
             ->once()
             ->with(true);
    
        $this->provider->boot();
    
        $mock->shouldHaveReceived('prohibitDestructiveCommands');
    }
);

It still has the problem, that the Facade is already loaded in that namespace and can't be overloaded, so it results in the test having the following error:

Mockery\Exception\RuntimeException:
    Could not load mock Illuminate\Support\Facades\DB,
    class already exists
at vendor/mockery/mockery/library/Mockery/Container.php:401
at vendor/mockery/mockery/library/Mockery.php:681
at vendor/mockery/mockery/library/helpers.php:40

If you can figure out how I can overcome this, or another way of asserting that a method from a principal facade class is invoked, then I would really appreciate the assistance.

Note: I know that I could wrap the method like so & mock the provider to assert the call of that method, however it's not a wonderful solution to enforcing behaviour through tests, as the call to DB::prohibitDestructiveCommands(app()->isProduction()) could be removed or changed by a dev at a later time from the wrapping method & tests wouldn't pickup the structural change.

I am also aware, I can test the outcome of the calling the prohibited commands, but this is a unit tests, and shouldn't be cross testing behaviour like this, just the structure of the class.

Upvotes: 1

Views: 44

Answers (1)

LordF
LordF

Reputation: 506

You shouldn't mock Facades like that. Laravel provides its own mocking support and I would advise you to use them.

Facades in Laravel should be mocked directly from their methods. Inside Illuminate\Support\Facades\Facade are protected methods that creates mocked instance called createFreshMockInstance and createMock which simple calls Mockery::mock but their I wrapper insinde other methods to avoid mocking same instance more than once, so to get that objects you should use Illuminate\Support\Facades\DB::shouldReceive();

Upvotes: 1

Related Questions