2045c856e9
- Add 10 core testing guides (setup, AAA principles, PHPUnit/Pest, mocking, anti-patterns, PHP 8.4+, debugging, data factories, advanced edge cases) - Add 13 component testing guides covering all Laravel layers - Add standard templates for PHPUnit 10/11 and Pest v2/v3 including ArchitectureTest - Add Docker smart test runner, dependency installer, and CI/CD templates (GitHub Actions, GitLab CI) - Add 4 Claude Code skills (laravel-test-setup, laravel-unit-test with --all mode, laravel-test-review, laravel-mock-helper) - Add single unified installer script (sg-install-unit-testing.sh) with global support, CLI runners, and 1-click updater (sg-test-update) - Add comprehensive documentation (README.md, USAGE-GUIDE.md) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3.3 KiB
3.3 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());
}
}