k0pernikus
k0pernikus

Reputation: 66500

How to mock Laravel Eloquent model with phpunit?

I have an Laravel User model based on the Illuminate\Database\Eloquent\Model.

It's used within a service that I want to unit test. That's why I want to mock the User with certain properties, in this case it's about the id which should be set to 23.

I create my mock via:

/**
 * @param int $id
 * @return User|\PHPUnit\Framework\MockObject\MockObject
 */
protected function createMockUser(int $id): \PHPUnit\Framework\MockObject\MockObject|User
{
    $user = $this
        ->getMockBuilder(User::class)
        ->disableOriginalConstructor()
        ->getMock();
    $user->id = $id;

    return $user;
}

I also tried:

$user->setAttribute('id', $id);

But in both cases, the id property on the mock will be null.

How do I set a property on a Laravel model's mock?

Upvotes: 3

Views: 8532

Answers (2)

iisisrael
iisisrael

Reputation: 603

While I agree with @mrhn that mocking Eloquent magic can get complicated quickly, and "you have to mock unnecessary long call chains", there's still value in having that ability. Testing everything with the Laravel app and container can become time consuming to run, and eventually less useful in a dev environment as that becomes a significant disincentive.

This answer regarding magic methods is the actual how to the OP. So your $user mock could look like this:

$user = $this
    ->getMockBuilder(User::class)
    ->disableOriginalConstructor()
    ->setMethods(['__get'])
    ->getMock()
;
$user->expects($this->any()
    ->method('__get')
    ->with('id')
    ->willReturn($id)
;

Upvotes: 2

mrhn
mrhn

Reputation: 18926

As stated in my comment, you should not Mock users. Laravel has multiple tools to combat this, main reason is you have to mock unnecessary long call chains, static functions and partial mocking won't work with table naming and save operations. Laravel has great testing documentation eg. testing with a database, which answers most of the best practices with testing in Laravel.


There is two approaches from here, you can create factories that can create users that are never save in the database. Calling create() on a factory will save it to the database, but calling make() will fill out the model with data and not saving it.

class UserFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
        ];
    }
}

// id is normally set by database, set it to something.
$user = User::factory()->make(['id' => 42]);

Instead with both unit testing and more feature oriented testing. It is way easier to just default to using sqlite both for performance and ease of use in a testing environment.

Add the following.

config/database.php

'sqlite' => [
    'driver' => 'sqlite',
    'url' => env('DATABASE_URL'),
    'database' => ':memory:',
    'prefix' => '',
    'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],

phpunit.xml

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

In your test use this trait.

use RefreshDatabase;

Now you should able to use the following. This will provide a complete User model, with all the bell and whistles you needs. The only downside with sqlite is due to which version you use, foreign keys and weird database specific features are not supported.

public function test_your_service()
{
    $result = resolve(YourService::class)->saveUserData(User::factory()->create());

    // assert
}

Upvotes: 4

Related Questions