2
votes

Been looking all over the internet but don't seem to find an answer to my problem. I've been diving into testing controllers in Laravel using PHPUnit and Mockery. However, I don't seem to get my Eloquent based models mocked correctly. I did manage to mock my Auth::user() the same way, although this is not used in the test below.

Function in AddressController that needs to be tested:

public function edit($id)
{
    $user = Auth::user();
    $company = Company::where('kvk', $user->kvk)->first();
    $address = Address::whereId($id)->first();

    if(is_null($address)) {
        return abort(404);
    }

    return view('pages.address.update')
        ->with(compact('address'));
}

ControllerTest containing setUp and mock method

abstract class ControllerTest extends TestCase
{
    /**
     * @var \App\Http\Controllers\Controller
     */
    protected $_controller;

    public function setUp(){
        parent::setUp();
        $this->createApplication();
    }

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

    protected function mock($class)
    {
        $mock = Mockery::mock($class);
        $this->app->instance($class, $mock);
        return $mock;
    }
}

AddressControllerTest extending ControllerTest

class AddressControllerTest extends ControllerTest
{
    /**
     * @var \App\Models\Address
     */
    private $_address;

    /**
     * @var \App\Http\Controllers\AddressController
     */
    protected $_controller;

    public function setUp(){
        parent::setUp();
        $this->_controller = new AddressController();
        $this->_address = factory(Address::class)->make();
    }

    public function testEdit404(){
        $companyMock = $this->mock(Company::class);
        $companyMock
            ->shouldReceive('where')
            ->with('kvk', Mockery::any())
            ->once();
            ->andReturn(factory(Company::class)->make([
                'address_id' => $this->_address->id
            ]));

        $addressMock = $this->mock(Address::class);
        $addressMock
            ->shouldReceive('whereId')
            ->with($this->_address->id)
            ->once();
            ->andReturn(null);

        //First try to go to route with non existing address
        $this->action('GET', 'AddressController@edit', ['id' => $this->_address->id]);
        $this->assertResponseStatus(404);
    }
}

The error it keeps throwing is:

1) AddressControllerTest::testEdit404
Mockery\Exception\InvalidCountException: Method where("kvk", object(Mockery\Matcher\Any)) from Mockery_1_Genta_Models_Company should be called exactly 1 times but called 0 times.

Perhaps anyone has an idea?

1
Replaced the $this->call() method with the $this->action() method while checking if my controller method got called at all and it did after replacement. However the problem persists.Kevin Wareman

1 Answers

9
votes

Okay, after finding multiple posts by Jeffrey Way (the guy behind Laracasts) recommending people to stop mocking Eloquent objects and instead use in memory databases I've tried that approach. I thought this would perhaps be very usable for other users having the same problems in the future, so I'll explain below.

Right now I've edited the my 'config/database.php' file to support in-memory database option using sqlite:

'sqlite' => [
            'driver'   => 'sqlite',
            'database' => ':memory:',
            'prefix'   => '',
        ],

Next on top of the file you'll see the following configuration:

'default' => env('DB_CONNECTION', 'mysql'),

This can stay the same, it means that Laravel will check your .env variables to find a 'DB_CONNECTION' or else use mysql as default. This is probably what you'd like to do when running your application as usual. However with testing you would like to override this configuration and set the database config to 'sqlite' temporarily. This can be done by adding the 'DB_CONNECTION' variable to your .env file:

DB_CONNECTION=mysql 

Finally in your phpunit.xml, the configuration file used by Laravel to instantiatie the unit tests, you have to tell that when testing this variable should be set to 'sqlite':

<env name="DB_CONNECTION" value="sqlite"/> 

Now you are done and Laravel will start up an invisible in-memory database everytime you are about to go testing. Don't forget to add the following line to tests that need the database.

use \Illuminate\Foundation\Testing\DatabaseMigrations;

It will tell Laravel run your database migrations before starting the tests, so you can use the tables like you normally would.

This way it works perfectly for me! Hope you guys can use it.