Testing Complex Queries in Laravel - from repository pattern to Laravel model mocking

Janez Cergolj • October 6, 2023

Today, we're diving into a discussion on testing complex queries inside a Laravel controller. We'll be looking at two debated topics: the repository pattern and how to mock Laravel eloquent models.

Imagine we have a block of code that looks like this:

User::search($request)
    ->onlyEditor()
    ->filterByStatus($request)
    ->orderBy('created_at', 'desc')
    ->paginate()

Here, we have three scope methods - search, onlyEditor, and filterByStatus, along with two default ones, orderBy and paginate.

Testing these methods directly within a Feature test can quickly become overwhelming and hard to manage. So, what's the best approach? Let's explore our options to find a more practical and efficient way to handle this.

The Repository Pattern in Laravel

The Laravel community has had its share of debates about the repository pattern. There was a time when it gained popularity and found its way into many projects. However, some began to question its practicality, viewing it as an additional layer without clear benefits. Yet, in certain scenarios, like the one we're exploring here, it proves to be a fitting choice.

<?php

namespace App\Http\Controllers;

use App\Repositories\UserRepository;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function search(Request $request, UserRepository $userRepository)
    {
        $results = $userRepository->searchAndFilter($request);

        return view('users.search_results', ['results' => $results]);
    }
}

The above code has a UserController that takes a request and a UserRepository as dependencies. It calls the searchAndFilter method of the repository to retrieve results.

namespace App\Repositories;

use App\Models\User;

class UserRepository
{
    protected $model;

    public function __construct(User $model)
    {
        $this->model = $model;
    }

    public function searchAndFilter($request)
    {
        return $this->model->search($request)
            ->onlyEditor()
            ->filterByStatus($request)
            ->orderBy('created_at', 'desc')
            ->paginate();
    }
}

In the UserRepository, we define a method searchAndFilter. This method combines model's methods: search, onlyEditor, filtersByStatus, orderBy, paginate, so all the methods that are part of the complex query.

<?php
namespace Tests\Feature;

use App\Models\User;
use App\Repositories\UserRepository;
use Mockery;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    public function test_search_and_filter_method_is_called()
    {
        $userRepository = Mockery::mock(UserRepository::class);
        $this->app->instance(UserRepository::class, $userRepository);

        $results = factory(User::class, 3)->create();

        $userRepository->shouldReceive('searchAndFilter')
            ->once()
            ->andReturn($results);

        $response = $this->get('/users/search');

        $response->assertStatus(200);
        $response->assertViewIs('users.search_results');
        $response->assertViewHas('results', $results);
    }

    // other test without mocking the user repository class
}

In the UserControllerTest, we are testing that the searchAndFilter method of the UserRepository is called when the /users/search endpoint is accessed. You know the normal feature test stuff. We use Mockery to create a mock instance of the repository, allowing us to set expectations on its behavior, but more importantly we are sure that method is being called.

<?php
use App\Repositories\UserRepository;
use Tests\TestCase;

class UserRepositoryTest extends TestCase
{
    public function test_search()
    {
        $userRepository = new UserRepository();
        $user1 = factory(\App\Models\User::class)->create(['name' => 'John Doe']);
        $user2 = factory(\App\Models\User::class)->create(['name' => 'Jane Doe']);

        $results = $userRepository->search('Doe');

        $this->assertCount(2, $results);
        $this->assertTrue($results->contains($user1));
        $this->assertTrue($results->contains($user2));
    }

    public function test_only_editor
    {
    ...
    }

    public function test_filter_by_status
    {
    ...
    }
}

In the UserRepositoryTest, we perform unit tests for the various methods of the UserRepository. For example, we test the search method, ensuring it retrieves the correct users based on the provided search term.

This code structure and testing approach offer a clear separation of concerns. The Feature test ensures that the repository class is being called correctly, while the unit tests for the repository methods validate their individual functionality.

An Alternative to the Repository Pattern: Custom Query Objects

If the repository pattern isn't your cup of tea, there's a similar approach that might be more to your liking. Instead of a UserRepository, you can opt for a UserQuery. The key difference here is that query classes usually have only one method, while repository classes often have several. You can find more information in this link to Laracast episode if you're curious about query objects.

namespace App\Queries;

use App\Models\User;

class SearchAndFilterQuery
{
    public function run($request)
    {
        return User::search($request)
            ->onlyEditor()
            ->filterByStatus($request)
            ->orderBy('created_at', 'desc')
            ->paginate();
    }
}

In this code, we have a SearchAndFilterQuery class inside the App\Queries namespace. It contains a single public method, run, which handles the search and filtering process and it returns paginated results. It's an alternative to the repository pattern that can be more appealing to some developers. Testing is done in similar manner to repository pattern, so I won't it convert here again.

Mocking the Laravel Eloquent Model

Now, for the fun part - let's talk about partially mocking the Eloquent User model. No extra classes involved here, just mocking of the User model!

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function search(Request $request, User $user)
    {
        $results = $user->search($request)
            ->onlyEditor()
            ->filterByStatus($request)
            ->orderBy('created_at', 'desc')
            ->paginate();

        return view('users.search_results', ['results' => $results]);
    }
}

In this code, we have a UserController that takes a request and a User model as dependencies. It calls various methods on the User model to perform a search and filter operation. User model is injected as second argument of index method.

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Http\Request;
use Mockery;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    public function test_search_and_filter_method_is_called()
    {
        $this->partialMock(User::class, function (MockInterface $mock) {
            User::factory()->create();

            $mock->shouldReceive('search')->with(Request::class)->once()->andReturn($mock);
            $mock->shouldReceive('onlyEditor')->once()->andReturn($mock);
            $mock->shouldReceive('filterByStatus')->once()->andReturn($mock);
            $mock->shouldReceive('orderBy')->once()->andReturn($mock);

            $mock->shouldReceive('paginate')->once()->andReturn(User::paginate());
        });

        $response = $this->get('/users/search');

        $response->assertStatus(200);
        $response->assertViewIs('users.search_results');
        $response->assertViewHas('results', $results);
    }

    // other test without mocking
}

Here, in our feature test, is the fun part with partially mocking User model. We're essentially saying, "Hey User model, when these specific methods are called, here's what you should do!" We're then asserting that our controller returns the expected results. And more importantly we asserts that those methods are being called, so we can unit test them elsewhere.

use App\Models\User;
use Tests\TestCase;

class UserTest extends TestCase
{
    public function test_search()
    {
    }

    public function test_only_editor()
    {
    }

    public function test_filter_by_status()
    {
    }
}

Here, We're testing the individual methods of the User model. We're ensuring that each method does what it's supposed to do.

Don't be ashamed of mocking nor using repository pattern

Not everyone in the Laravel community is a big fan of using the repository pattern or mocking Laravel models. I understand their point, and I often agree.

However, sometimes you'll come across situations where mocking Laravel Eloquent models or using repository pattern makes sense. Don't be afraid of using them just because someone told you that you shouldn't.