Search code examples
phplaravelphpunitdingo-api

Is there a way to act as a user inside of Laravel without hitting the database?


I'm writing unit tests for an API using PHPUnit and Laravel. Most functions I'm testing require that the user is authenticated before the function can be ran. The user data is stored in one table, and their permissions are stored inside of another table. I can fake the user object inside of Laravel, but I need to be able to also pull the corresponding permissions from the other table without having to hit the database like the dingo router currently is doing.

Currently running Laravel 5.8 and PHPUnit 8.1.5. I currently have the users object that I generated from a Laravel factory saved to a text file. I am able to pass that to a function called "actingAsApi" (found on Github, code below) and that allows me to authenticate as that user. However, the function is still going out and getting all permissions for that user from the database. I'm trying to mock or fake the permissions object it is pulling somewhere so that it doesn't need to hit the database at all. I also tried using the built in Passport functions for Passport::actingAs, and those did not work either as they were still hitting the DB (and not really working anyways).

actingAsApi (inside of TestCase.php)

protected function actingAsApi($user)
    {
        // mock service middleware
        $auth = Mockery::mock('Dingo\Api\Http\Middleware\Auth[handle]',
            [
                Mockery::mock('Dingo\Api\Routing\Router'),
                Mockery::mock('Dingo\Api\Auth\Auth'),
            ]);
        $auth->shouldReceive('handle')
            ->andReturnUsing(function ($request, \Closure $next) {
                return $next($request);
            });
        $this->app->instance('Dingo\Api\Http\Middleware\Auth', $auth);
        $auth = Mockery::mock('Dingo\Api\Auth\Auth[user]',
            [
                app('Dingo\Api\Routing\Router'),
                app('Illuminate\Container\Container'),
                [],
            ]);
        $auth->shouldReceive('user')
            ->andReturnUsing(function () use ($user) {
                return $user;
            });
        $this->app->instance('Dingo\Api\Auth\Auth', $auth);
        return $this;
    }

Test inside of my Test file

public function testActAs() {
    $user = 'tests/users/user1.txt';
    $this->actingAsApi($user);

    $request = new Request;
    $t = new TestController($request);

    $test = $t->index($request);
}

I expect the actingAsApi function to allow me to also pass in the mock permissions data that corresponds to my mock user object data from the file, but instead it is hitting the database to pull from the permissions table.

EDIT:

So i've been playing around with doing mock objects, and i figured out how to mock the original controller here:

$controlMock = Mockery::mock('App\Http\Controllers\Controller', [$request])->makePartial();

$controlMock->shouldReceive('userHasPermission')
            ->with('API_ACCESS')
            ->andReturn(true);
                   
$this->app->instance('App\Http\Controllers\Controller', $controlMock);

but now I can't figure out how to get my call from the other controllers to hit the mocked controller and not a real one. Here is my code for hitting an example controller:

$info = $this->app->make('App\API\Controllers\InfoController');

print_r($info->getInfo('12345'));

How can i make the second block of code hit the mocked controller and not standup a real one like it does in its constructor method?


Solution

  • Finally came on an answer, and it is now fixed. Here's how I did it for those wondering:

    $request = new Request;
    
    $controlMock = m::mock('App\API\Controllers\InfoController', [$request])->makePartial();
    $controlMock->shouldReceive('userHasPermission')
                 ->with('API_ACCESS')
                 ->andReturn(true);
    
    print_r($controlMock->getInfo('12345'));
    

    Basically, I was trying to Mock the original API controller, and then catch all of the calls thrown at it. Instead, I should've been mocking the controller I'm testing, in this case the InfoController. I can then catch the call 'userHasPermission', which should reach out to the Controller, but I am automatically returning true. This eliminates the need for hitting the database to receive permissions and other info. More information on how I solved it using Mockery can be found here: http://docs.mockery.io/en/latest/cookbook/big_parent_class.html. As you can see, this is referred to as a 'Big Parent Class'. Good luck!