Files
agent-unit-testing/docs/components/13-observers-and-exceptions.md
T
Pham-Tan-Thien 2045c856e9 feat: complete Smartgift AI Laravel unit testing toolkit
- 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>
2026-09-07 16:41:37 +07:00

3.8 KiB

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

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+

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

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+

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']);
    }
}