# Component Guide: Repositories & Custom Query Builders Kiểm thử tầng Repository, Custom Query Builder và Eloquent Scopes đòi hỏi chiến lược đặc thù vì chúng tương tác trực tiếp với cơ sở dữ liệu. --- ## 1. Bản Chất & Chiến Lược Kiểm Thử ### Tại sao KHÔNG nên Mock Eloquent Query Builder? Eloquent Builder có hàng trăm phương thức liên kết chuỗi (`where`, `orWhere`, `whereHas`, `orderBy`, `limit`). Việc mock toàn bộ chuỗi này vừa cực kỳ tốn công sức vừa tạo ra các bài test **rất dễ vỡ (brittle)**. ### Giải Pháp Chuẩn: SQLite In-Memory Database Đối với Repository và Query Builder, hãy sử dụng **SQLite In-Memory** (`:memory:`): - Chạy cực nhanh (dưới 30ms). - Sử dụng trait `Illuminate\Foundation\Testing\RefreshDatabase`. - Cho phép thực thi câu lệnh SQL thật để đảm bảo query trả về đúng dữ liệu, đúng điều kiện lọc và đúng sắp xếp. --- ## 2. Mã Mẫu Chuẩn: Eloquent Scope & Repository ### Class Nguồn: `App\Repositories\UserRepository` ```php namespace App\Repositories; use App\Models\User; use Illuminate\Database\Eloquent\Collection; class UserRepository { public function getActiveUsersOlderThan(int $age): Collection { return User::query() ->where('is_active', true) ->where('age', '>', $age) ->orderBy('name') ->get(); } } ``` ### Test Chuẩn: PHPUnit 10+ (với SQLite In-Memory) ```php namespace Tests\Unit\Repositories; use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; use PHPUnit\Framework\Attributes\Test; use App\Models\User; use App\Repositories\UserRepository; class UserRepositoryTest extends TestCase { use RefreshDatabase; // Tự động migrate trên SQLite in-memory private UserRepository $repository; protected function setUp(): void { parent::setUp(); $this->repository = new UserRepository(); } #[Test] public function it_returns_only_active_users_older_than_specified_age_ordered_by_name(): void { // Arrange: Tạo dữ liệu kiểm thử // Thỏa mãn cả 2 điều kiện $validUserB = User::factory()->create(['name' => 'Binh', 'is_active' => true, 'age' => 25]); $validUserA = User::factory()->create(['name' => 'An', 'is_active' => true, 'age' => 30]); // Không thỏa mãn is_active User::factory()->create(['name' => 'Cuong', 'is_active' => false, 'age' => 28]); // Không thỏa mãn age (<= 20) User::factory()->create(['name' => 'Dung', 'is_active' => true, 'age' => 18]); // Act $results = $this->repository->getActiveUsersOlderThan(20); // Assert $this->assertCount(2, $results); $this->assertSame($validUserA->id, $results->first()->id); // Kiểm tra thứ tự A trước B $this->assertSame($validUserB->id, $results->last()->id); } #[Test] public function it_returns_empty_collection_when_no_user_matches(): void { // Arrange User::factory()->create(['is_active' => false, 'age' => 50]); // Act $results = $this->repository->getActiveUsersOlderThan(40); // Assert $this->assertTrue($results->isEmpty()); } } ```