0dce2ef812
- Enforce mandatory handle() and failed() method testing for all Jobs and Commands - Ban shallow reflection-only helper testing in Jobs - Mandate actual SQLite in-memory query execution for Repositories (ban Builder::class assertions alone) - Enforce full branch coverage for SonarQube (ternary, null-coalescing, catch blocks, match) - Update JobTest templates for PHPUnit and Pest to include handle() and failed() methods - Update verification checklist and master system prompt with strict branch gatekeepers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.6 KiB
4.6 KiB
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
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)
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:
$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:
- Kết hợp trait
use RefreshDatabase;với SQLite in-memory siêu tốc. - 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. - 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. - 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(...).
- Kết hợp trait