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
118 lines
4.6 KiB
Markdown
118 lines
4.6 KiB
Markdown
# 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ủ thực tế (Coverage) 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(...)`.
|