How to mock ActiveRecord

If you want to mock AR that doesn’t try to connect to database you can do it in the following way (if using PHPUnit):

$post = $this->getMockBuilder('\app\model\Post')
    ->setMethods(['save', 'attributes'])
    ->getMock();

$post->method('save')->willReturn(true);
$post->method('attributes')->willReturn([
    'id',
    'status',
    'title',
    'description',
    'text'
]);

The catch is that we need to override attributes() method since ActiveRecord by default is getting attributes list from database schema which we’re trying to avoid.

5 Likes

Based on this solution, here is the way to stub AR relations:

$attributes = (new Post)->attributes();
$relations = ['comments', 'author'];

/** @var Post|MockObject $task */
$post = $this->getMockBuilder(Post::class)
    ->setMethods(['attributes'])
    ->getMock();

// Stub relations by turn them as an usual model attributes.
$post->method('attributes')
    ->willReturn(array_merge($attributes, $relations));

// Now we can assign values to relations that in normal 
// circumstances impossible due to "read only" access to them.

$post->comments = [new Comment];
$post->author = new Author;

// Assign an ordinary attribute, for an example

$post->title = 'the title of the post';
2 Likes

Instead of hard coded class name, which one is considered a better practice:
$post = $this->getMockBuilder(Post::className())
or even:
$post = $this->getMockBuilder(Post::class)

1 Like

Note: This kind of destroys the normal workflow, since validate() is no longer run. The code has to manually run validate() before running save(), which is a bit awkward. Maybe there’s a better way to mock this particular method? Example:

        $post->method('save')->will(
            $this->returnCallback(
                function() use ($post) {
                    return $post->validate();
                }
            )
        );

(Running Yii 1.1.)