# Component Guide: Observers & Custom Exceptions Kiểm thử Model Observers và Custom Exception Handlers đảm bảo các hành động phản ứng tự động (Side-effects) và xử lý lỗi được thực hiện đồng nhất. --- ## 1. Kiểm Thử Model Observers - **Bản chất**: Observer lắng nghe các lifecycle events của Model (`creating`, `created`, `updating`, `deleted`,...). - **Cách test đơn vị**: Khởi tạo trực tiếp instance của Observer và gọi phương thức (ví dụ: `$observer->creating($model)`) mà không cần lưu vào database. ### Observer Nguồn: `App\Observers\UserObserver` ```php namespace App\Observers; use App\Models\User; use Illuminate\Support\Str; class UserObserver { public function creating(User $user): void { // Tự động gán UUID nếu chưa có if (empty($user->uuid)) { $user->uuid = (string) Str::uuid(); } // Tự động chuẩn hóa email về chữ thường $user->email = strtolower($user->email); } } ``` ### Test Chuẩn: PHPUnit 10+ ```php namespace Tests\Unit\Observers; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\Test; use App\Observers\UserObserver; use App\Models\User; class UserObserverTest extends TestCase { #[Test] public function it_generates_uuid_and_lowercases_email_on_creating(): void { // Arrange $user = new User(['email' => 'JOHN.DOE@EXAMPLE.COM']); $observer = new UserObserver(); // Act $observer->creating($user); // Assert $this->assertNotEmpty($user->uuid); $this->assertSame('john.doe@example.com', $user->email); } } ``` --- ## 2. Kiểm Thử Custom Exceptions Kiểm thử Custom Exception đảm bảo: - Exception mang đúng mã HTTP status code. - Exception render đúng cấu trúc JSON khi được kích hoạt trong API context. ### Exception Nguồn: `App\Exceptions\InsufficientBalanceException` ```php namespace App\Exceptions; use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class InsufficientBalanceException extends Exception { public function __construct( public readonly int $currentBalance, public readonly int $requiredAmount ) { parent::__construct('Số dư tài khoản không đủ để thực hiện giao dịch.'); } public function render(Request $request): JsonResponse { return response()->json([ 'status' => 'error', 'code' => 'INSUFFICIENT_BALANCE', 'message' => $this->getMessage(), 'current_balance' => $this->currentBalance, 'required_amount' => $this->requiredAmount, ], Response::HTTP_PAYMENT_REQUIRED); } } ``` ### Test Chuẩn: PHPUnit 10+ ```php namespace Tests\Unit\Exceptions; use Tests\TestCase; use PHPUnit\Framework\Attributes\Test; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; use App\Exceptions\InsufficientBalanceException; class InsufficientBalanceExceptionTest extends TestCase { #[Test] public function it_renders_custom_json_response_with_balance_details(): void { $exception = new InsufficientBalanceException(currentBalance: 50_000, requiredAmount: 120_000); $request = Request::create('/api/payment', 'POST'); $response = $exception->render($request); $this->assertSame(Response::HTTP_PAYMENT_REQUIRED, $response->getStatusCode()); $data = json_decode($response->getContent(), true); $this->assertSame('INSUFFICIENT_BALANCE', $data['code']); $this->assertSame(50_000, $data['current_balance']); $this->assertSame(120_000, $data['required_amount']); } } ```