0

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?

k0pernikus
  • 60,309
  • 67
  • 216
  • 347
  • I worked around my issue by not injecting the model in my service but rather directly its id, still for certain service that approach would blow up (assuming I want to access multiple values on it). In this case, it suffices. – k0pernikus Nov 10 '21 at 18:18
  • 4
    You can create an empty `User::class` model and set the id: `new User(['id' => 23]);` Or use a model factory: https://laravel.com/docs/master/database-testing#defining-model-factories – Dennis Nov 10 '21 at 18:24
  • 1
    Mocking Laravel models does not solve anything, it can be created with factories and instead of using an database the functionality works with an sqlite database or similar. Even empty models without database access would work if you only has to access properties. – mrhn Nov 10 '21 at 22:37

2 Answers2

4

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
}
mrhn
  • 17,961
  • 4
  • 27
  • 46
  • Thanks for the answer, I would have to write something similar but you saved me time. I will just add some links to the **documentation** that any user should read before asking any question, as Laravel has one of the best documentations ever created... Everything is explained there and most of the time pretty clear... Now, for the author: the documentation has its own [`testing`](https://laravel.com/docs/8.x/database-testing#introduction) section, so it would be awesome if you read that to complement anything you can find here as an answer... – matiaslauriti Nov 11 '21 at 03:20
  • I don't need any db operation like save. Basically, I use the mock as a pdo while conforming to the class name. I don't need a SQLite db, as the service under test only reads data from the model instance. – k0pernikus Nov 11 '21 at 05:37
  • Then use the first solution, no db needed there. But then relationships wont work. I have seen multiple persons trying to mock their way models and it is timeconsuming and not practical and all laravel developers that tests does something similar to this. Then you can argue all about what you need and what you do other places. But this is the standard in laravel and in general it has a very pragmatic approach to most problems and taylor stated that he always wants a test that doesnt use mocks so he knows the implementation works – mrhn Nov 11 '21 at 07:09
0

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)
;
iisisrael
  • 583
  • 6
  • 6