# 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()); } } ``` --- ## 3. Bẫy Nghiêm Trọng Cần Tránh Khi Test Repository ### ❌ Bẫy Anti-Pattern: Chỉ kiểm tra `assertInstanceOf(Builder::class)` Nhiều bài test chỉ viết: ```php $query = $repo->search(['name' => 'ABC']); $this->assertInstanceOf(Builder::class, $query); // ❌ SAI LẦM: Chỉ chạm đúng 1-2 dòng return! ``` - **Hậu quả**: Toàn bộ logic bên trong các câu `where()`, `join()`, `groupBy()`, `orderBy()`, xử lý lọc ngày tháng, lọc trạng thái... **hoàn toàn không được thực thi**, dẫn đến độ phủ SonarQube của Repository chỉ đạt 10% - 25%! - **Quy tắc vàng**: 1. Kết hợp trait `use RefreshDatabase;` với SQLite in-memory siêu tốc. 2. Tạo bản ghi mẫu bằng `Model::factory()->create(...)` thỏa mãn và không thỏa mãn điều kiện lọc. 3. Thực thi query lấy kết quả thật (`$results = $query->get()` hoặc gọi thẳng method `$repo->findActive(...)`) và assert chính xác số lượng bản ghi (`assertCount`) và ID của các bản ghi thỏa mãn. 4. Mọi phương thức update, delete, bulkInsert của Repository đều phải được gọi và assert trạng thái database bằng `$this->assertDatabaseHas(...)` hoặc `$this->assertDatabaseMissing(...)`.