da1bd48444
- Decouple framework installation: add install-laravel.sh and install-cakephp.sh, isolate skills and docs per framework, and auto-clean leftover artifacts - Remove all SonarQube configs, templates, and references - Enforce comprehensive test completeness: 100% public methods, edge cases, full branch coverage, exception paths, and state assertions - Replace all placeholder assertions in PHPUnit and Pest templates with full realistic implementations - Rename and update 10-comprehensive-test-coverage-guide.md
125 lines
4.2 KiB
Markdown
125 lines
4.2 KiB
Markdown
# 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.
|