da1bd48444
- Decouple framework installation: add install-laravel.sh and install-cakephp.sh, isolate skills and docs per framework, and auto-clean leftover artifacts - Remove all SonarQube configs, templates, and references - Enforce comprehensive test completeness: 100% public methods, edge cases, full branch coverage, exception paths, and state assertions - Replace all placeholder assertions in PHPUnit and Pest templates with full realistic implementations - Rename and update 10-comprehensive-test-coverage-guide.md
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ủ thực tế (Coverage) 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