Complex Queries
When scopes multiply, extract a query object
Imagine a controller with this query chain:
// app/Http/Controllers/UserController.php
$results = User::search($request)
->onlyEditor()
->filterByStatus($request)
->orderBy('created_at', 'desc')
->paginate();
Testing this in a feature test means creating users with different roles, statuses, and dates. One small change breaks everything.
The Solution: Query Object
// app/Queries/SearchAndFilterUsersQuery.php
class SearchAndFilterUsersQuery
{
public function run($request)
{
return User::search($request)
->onlyEditor()
->filterByStatus($request)
->orderBy('created_at', 'desc')
->paginate();
}
}
// app/Http/Controllers/UserController.php
public function index(Request $request, SearchAndFilterUsersQuery $query)
{
$results = $query->run($request);
return view('users.index', compact('results'));
}
Feature Test — mock the query object
// tests/Feature/Http/Controllers/UserController/IndexTest.php
$query = Mockery::mock(SearchAndFilterUsersQuery::class);
$this->app->instance(SearchAndFilterUsersQuery::class, $query);
$users = User::factory()->count(3)->create();
$query->shouldReceive('run')
->once()
->andReturn($users);
$response = $this->get('/users');
$response->assertViewIs('users.index')
->assertViewHas('results', $users);
Unit Test — test the query object
// tests/Unit/Queries/SearchAndFilterUsersQueryTest.php
class SearchAndFilterUsersQueryTest extends TestCase
{
#[Test]
public function run_finds_users_by_name(): void
{
$user1 = User::factory()->create(['name' => 'John']);
$user2 = User::factory()->create(['name' => 'Jane']);
$query = new SearchAndFilterUsersQuery;
$results = $query->run((object)['search' => 'John']);
$this->assertTrue($results->contains($user1));
$this->assertFalse($results->contains($user2));
}
}