Files
agent-unit-testing/docs/09-advanced-edge-cases.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

5.9 KiB

09. Các Tình Huống Kỹ Thuật Đặc Biệt (Advanced Edge Cases)

Tài liệu hướng dẫn xử lý 7 tình huống kỹ thuật phức tạp thường gặp trong các dự án Laravel thực tế mà lập trình viên và AI hay bị lúng túng.


1. Mock DB::transaction() Trong Unit Test (Không Cần Database Thật)

Nhiều Service bọc logic thanh toán/tạo đơn trong DB::transaction(function () { ... }). Khi viết Unit Test cô lập (không kết nối DB), nếu không mock lệnh này, code sẽ văng lỗi connection.

✅ Giải pháp chuẩn:

Sử dụng DB::shouldReceive('transaction') và thực thi ngay closure truyền vào:

use Illuminate\Support\Facades\DB;

#[Test]
public function it_executes_logic_inside_database_transaction(): void
{
    // Giả lập DB::transaction để chạy trực tiếp callback bên trong
    DB::shouldReceive('transaction')
        ->once()
        ->andReturnUsing(function (callable $callback) {
            return $callback();
        });

    // Act
    $result = $this->orderService->createOrder($orderDTO);

    // Assert
    $this->assertNotNull($result);
}

2. Kiểm Thử Trait Trong Sự Cô Lập (Testing Traits in Isolation)

Khi logic nghiệp vụ nằm trong một Trait (ví dụ: GeneratesInvoiceCodeTrait, HasSlugTrait), làm sao để test mà không cần khởi tạo một Model hoặc Service cụ thể?

✅ Giải pháp chuẩn: Dùng Anonymous Class

#[Test]
public function it_generates_unique_invoice_code_from_trait(): void
{
    // Tạo một anonymous class sử dụng Trait cần test
    $instance = new class {
        use GeneratesInvoiceCodeTrait;

        // Bổ sung các method/property trừu tượng nếu trait yêu cầu
        public function prefix(): string {
            return 'INV';
        }
    };

    $code = $instance->generateInvoiceCode(123);

    $this->assertMatchesRegularExpression('/^INV-123-[A-Z0-9]{4}$/', $code);
}

3. Thao Tác Cấu Hình (config()) & Biến Môi Trường Trong Test

Nhiều class đọc cấu hình qua config('services.stripe.secret').

✅ Quy tắc chuẩn:

  • KHÔNG BAO GIỜ sửa file .env thật trong lúc test.
  • Sử dụng Config::set() trực tiếp trong phần // Arrange:
    use Illuminate\Support\Facades\Config;
    
    #[Test]
    public function it_uses_configured_vat_rate(): void
    {
        // Gán tạm thời cấu hình cho bài test này
        Config::set('finance.vat_rate', 0.08); // 8%
    
        $tax = $this->taxService->calculate(100_000);
        $this->assertSame(8_000.0, $tax);
    }
    

4. Kiểm Thử Upload File & Multi-part Request (UploadedFile::fake())

Kiểm thử các service xử lý file ảnh, tài liệu PDF, file CSV mà không cần file thật trên ổ cứng:

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

#[Test]
public function it_uploads_and_validates_pdf_invoice(): void
{
    Storage::fake('documents');

    // Tạo file PDF giả lập trong bộ nhớ
    $fakePdf = UploadedFile::fake()->create('invoice.pdf', 500, 'application/pdf');

    $uploadedPath = $this->documentService->storeInvoice($fakePdf);

    Storage::disk('documents')->assertExists($uploadedPath);
}

#[Test]
public function it_rejects_file_exceeding_max_size(): void
{
    // Giả lập file ảnh vượt quá 10MB (11,000 KB)
    $heavyImage = UploadedFile::fake()->image('huge.jpg')->size(11_000);

    $this->expectException(FileTooLargeException::class);
    $this->avatarService->upload($heavyImage);
}

5. Kiểm Thử Lỗi Serialization Của Queueable Jobs (SerializesModels)

Trong Laravel, khi truyền một Eloquent Model vào Job, Laravel sẽ serialize model bằng ID của nó. Nếu truyền một Model chưa được lưu vào DB (ID = null), Job sẽ crash trong worker thực tế.

✅ Cách kiểm tra độ an toàn của Job Payload:

#[Test]
public function it_can_be_serialized_and_unserialized_safely(): void
{
    $user = User::factory()->make(['id' => 101]);
    $job = new SendWelcomeEmailJob($user);

    // Thử serialize và unserialize giống hệt cơ chế của Queue Worker
    $serialized = serialize($job);
    $unserializedJob = unserialize($serialized);

    $this->assertInstanceOf(SendWelcomeEmailJob::class, $unserializedJob);
    $this->assertSame(101, $unserializedJob->user->id);
}

6. Xử Lý Giả Lập Session & Authentication Trong Unit Test Thuần

Khi class nguồn gọi auth()->user(), auth()->id() hoặc session()->get():

use Illuminate\Support\Facades\Auth;
use App\Models\User;

#[Test]
public function it_retrieves_data_for_authenticated_user(): void
{
    // Arrange: Giả lập user đã đăng nhập vào Auth Facade
    $mockUser = new User(['id' => 99, 'name' => 'Thien Phan']);
    Auth::shouldReceive('user')->andReturn($mockUser);
    Auth::shouldReceive('id')->andReturn(99);
    Auth::shouldReceive('check')->andReturnTrue();

    // Act
    $result = $this->userProfileService->getCurrentUserProfile();

    // Assert
    $this->assertSame('Thien Phan', $result->name);
}

7. Kiểm Thử Sự Kiện Đa Kênh & Tránh Cascading Events

Khi một hành động kích hoạt Event, và Event đó lại kích hoạt nhiều Listener khác có thể ghi dữ liệu hoặc gửi mail:

use Illuminate\Support\Facades\Event;
use App\Events\UserRegisteredEvent;

#[Test]
public function it_dispatches_registered_event_without_firing_listeners(): void
{
    // Chặn tất cả Listener chạy ngầm, chỉ xác nhận Event được bắn ra
    Event::fake([
        UserRegisteredEvent::class
    ]);

    $this->registrationService->register($userData);

    // Xác nhận event được dispatch kèm payload chuẩn
    Event::assertDispatched(UserRegisteredEvent::class, function ($event) use ($userData) {
        return $event->userEmail === $userData['email'];
    });
}