How mock the model without connection database in Laravel

Viewed 2494

I try to test the method index() of the controller. In this method, there's a model.

class UserController extends Controller
{        
     public function index()
     {
        return User::all();
     }
}

In the test class, I have the following.

class UserControllerTest extends TestCase
{
    public function testIndex():void
    {
        $user = factory(User::class)->make();
        $mock = Mockery::mock(User::class);
        $mock->shouldReceive('all')->andReturn($user);
        $this->app->instance('User', $mock);
        $response = $this->json('GET', 'api/users');
        dd($response->getContent()); // error : [2002] Connection refused
    }

}

When I run the test I have an error with the connection to the database. This is weird because I have mocked the model which would mean that I don't need to establish a connection to the database. How can I solve this error?

Error

SQLSTATE[HY000] [2002] Connection refused (SQL: select * from users where users.deleted_at is null)

1 Answers

You are trying to mock a static call with the approach for mocking a function call to an object instance. Mocking static function calls is not straight forward, it can be done with aliasing, but is not recommended.

A simple approach is to simple wrap your logic in a service and mock it.

class UserService {

    public function all(): Collection {
        return User::all();
    }
}

Now you mock code should look like this.

    $user = factory(User::class)->make();
    $mock = Mockery::mock(UserService::class);
    // Teoretically all method will return Eloquent Collection, but should be fine 
    $mock->shouldReceive('all')->andReturn(collect($user));
    $this->app->instance(UserService::class, $mock);

When using the container and replacing instances, it is very dependent you get these mocked classes out with the container and not the new keyword. Therefor the controller should look something similar to this.

class UserController extends Controller
{
    /** @var UserService **/
    private $userService;

    public function __construct(UserService $userService) {
        // load userService from the container as the mocked instance on tests
        $this->userService = $userService;
    }

    public function index()
    {
        return $this->userService->all();
    }
}

The final note, i have over multiple projects been a main driver of testings, code coverage etc. it is way easier to create tests by having the database there, either use sqlite or have a docker environment provide the database for you. Testing without is more a hurdle than to provide anything significant of value. Speed is crucial in your testing approach, because there is gonna be a lot of it and it is better to do it quickly then to skip it due to time pressure and mocking all db calls is gonna be hard.

Related