Search code examples
phpunit-testinglaravelmockingmockery

Mocking a Laravel model not working - Goes for the database anyway


I'm currently trying to follow the repository pattern and I've gotten to the part where I need to test the repositories. What I'm trying to test is calling the create function and mocking the result without actually hitting the database. But when running the test, I end up with the error:

PDOException: SQLSTATE[HY000] [2003] Can't connect to MySQL server on 'mysql' (113)

Here's what my test file looks like:

<?php

class UserRepositoryTest extends TestCase
{
    private $user;

    public function __construct()
    {
        $this->mock = Mockery::mock('App\User');
        $this->user = new \App\Repositories\UserRepository(new Illuminate\Container\Container, new \Illuminate\Support\Collection);
    }

    public function setUp()
    {
        parent::setUp();
    }

    public function tearDown()
    {
        Mockery::close();
    }

    public function testCreate()
    {
        $this->mock
             ->shouldReceive('create')
             ->withAnyArgs()
             ->once()
             ->andReturn(array());

        $user = $this->user->create(['password' => 'secret']);
    }

} 

Here's what the user repository looks like:

<?php

namespace App\Repositories;

use App\Repositories\Eloquent\Repository;
use Illuminate\Support\Facades\Hash;

class UserRepository extends Repository
{
    /**
     * Specify Model class name.
     *
     * @return mixed
     */
    public function model()
    {
        return 'App\User';
    }

    /**
     * @param array $data
     *
     * @return mixed
     */
    public function create(array $data)
    {
        $data['partner_status'] = 'Active';
        $data['password'] = Hash::make($data['password']);

        return parent::create($data);
    }
}

And the parent's create function looks like this:

/**
 * @param array $data
 *
 * @return mixed
 */
public function create(array $data)
{
    return $this->model->create($data);
}

How do I mock it so it actually return what I tell it to return?


Solution

  • Mocking an object creates one specific mocked instance. It does not affect any other code that creates new instances of that type of object. For example:

    $mock = Mockery::mock('App\User');
    $user = new \App\User();
    

    In the above example, $mock will be an instance of your mocked object, but $user will still be a normal User object, not related to the mock at all.

    This test is actually leading you down the path to a better design. Currently, your UserRepository has a hidden dependency on the \App\User class. To tackle this issue, you should add the dependency to the constructor of your repository, so it can be injected when the repository is created.

    By adding the dependency to the constructor, it is no longer hidden, and you will be able to inject your mocked object, instead of having the repository attempt to create new objects, which are more difficult to test, as you've found out.

    So, your repository will look something like:

    class UserRepository extends Repository
    {
        public function __construct(\App\User $model, \Illuminate\Container\Container $container, \Illuminate\Support\Collection $collection) {
            $this->model = $model;
            $this->container = $container;
            $this->collection = $collection;
        }
    }
    

    And then your test would look something like:

    class UserRepositoryTest extends TestCase
    {
        public function __construct()
        {
            $this->mock = Mockery::mock('App\User');
        }
    
        public function testCreate()
        {
            $this->mock
                 ->shouldReceive('create')
                 ->withAnyArgs()
                 ->once()
                 ->andReturn(array());
            $repo = new \App\Repositories\UserRepository($this->mock, new Illuminate\Container\Container, new \Illuminate\Support\Collection);
    
            $user = $repo->create(['password' => 'secret']);
        }
    }