# Component Guide: Queueable Jobs Jobs xử lý các tác vụ nền không đồng bộ (Asynchronous Background Tasks) như gửi email hàng loạt, xử lý file nặng, đồng bộ dữ liệu. --- ## 1. Hai Mức Độ Kiểm Thử Cho Jobs 1. **Kiểm thử Dispatching (Job được đẩy vào hàng đợi đúng lúc không?)**: - Dùng `Queue::fake()` trong Service/Controller test. - Xác nhận `Queue::assertPushed(MyJob::class)`. 2. **Kiểm thử Thực Thi (Unit Test trực tiếp method `handle()` của Job)**: - Đây chính là đối tượng của Unit Test Job. - Khởi tạo trực tiếp đối tượng `$job = new ProcessPaymentJob(...)`. - Inject mock dependencies vào method `handle()`. - Kiểm tra xem Job có thực thi logic và gọi service đúng không. - Kiểm tra method `failed(\Throwable $exception)` khi gặp sự cố. --- ## 2. Mã Mẫu Chuẩn: Job Unit Test ### Job Nguồn: `App\Jobs\SendWelcomeEmailJob` ```php namespace App\Jobs; use App\Contracts\MailerInterface; use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class SendWelcomeEmailJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 3; public function __construct( public readonly User $user ) {} public function handle(MailerInterface $mailer): void { $mailer->sendWelcome($this->user->email, $this->user->name); } public function failed(?\Throwable $exception): void { \Illuminate\Support\Facades\Log::error("Failed to send welcome email to user {$this->user->id}"); } } ``` ### Test Chuẩn: PHPUnit 10+ ```php namespace Tests\Unit\Jobs; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\Test; use Mockery; use App\Jobs\SendWelcomeEmailJob; use App\Contracts\MailerInterface; use App\Models\User; use Illuminate\Support\Facades\Log; class SendWelcomeEmailJobTest extends TestCase { protected function tearDown(): void { Mockery::close(); parent::tearDown(); } #[Test] public function it_sends_welcome_email_when_handled(): void { // Arrange $user = new User(['id' => 10, 'name' => 'Thien', 'email' => 'thien@example.com']); $mailerMock = Mockery::mock(MailerInterface::class); $mailerMock->shouldReceive('sendWelcome') ->once() ->with('thien@example.com', 'Thien'); $job = new SendWelcomeEmailJob($user); // Act $job->handle($mailerMock); // Assert: Mockery tự động verify qua expectation ->once() $this->assertTrue(true); } #[Test] public function it_logs_error_when_job_fails(): void { // Arrange $user = new User(['id' => 10, 'name' => 'Thien', 'email' => 'thien@example.com']); $job = new SendWelcomeEmailJob($user); Log::shouldReceive('error') ->once() ->with('Failed to send welcome email to user 10'); // Act $job->failed(new \RuntimeException('SMTP Connection Timeout')); } } ``` --- ## 3. Bẫy Nghiêm Trọng Cần Tránh Khi Test Jobs ### ❌ Bẫy Anti-Pattern: Chỉ dùng Reflection test hàm helper nhỏ Nhiều AI và lập trình viên hay dùng `ReflectionMethod` để test các hàm helper nhỏ như `buildRedisKey()`, `getFilePath()` và bỏ qua hoàn toàn phương thức chính **`handle()`**. - **Hậu quả**: Độ phủ mã nguồn thực tế của Job chỉ đạt 1% - 10%, vì 90% dòng code nghiệp vụ nằm trong `handle()` không hề được chạy! - **Quy tắc vàng**: 1. **BẮT BUỘC 100% phải test method `handle()`**. 2. Mock toàn bộ dependencies của `handle()`: Storage (`Storage::fake()`), Database/Repository, Redis (`Redis::shouldReceive()`), Mailer. 3. Phải có bài test cho các trường hợp rẽ nhánh trong `handle()`: khi dữ liệu rỗng, khi export thành công, khi import lỗi từng dòng. 4. Phải có bài test gọi method `failed(?Throwable $e)` để kiểm tra việc ghi log hoặc bắn alert khi job thất bại.