Queue Monitor for Laravel
Testing Guide
Testing Guide
Running Tests
All Tests
composer test
With Coverage
composer test-coverage
Specific Test Suite
vendor/bin/pest tests/Unit
vendor/bin/pest tests/Feature
Specific Test File
vendor/bin/pest tests/Unit/Enums/JobStatusTest.php
Test Organization
tests/
├── Unit/ # Isolated unit tests
│ ├── Enums/ # Enum behavior tests
│ ├── DataTransferObjects/ # DTO serialization tests
│ └── Models/ # Model behavior tests
├── Feature/ # Integration tests
│ ├── Actions/ # Action execution tests
│ ├── Api/ # API endpoint tests
│ ├── Commands/ # Artisan command tests
│ └── Repositories/ # Repository query tests
└── Pest.php # Global test configuration
Test Database
Tests use SQLite in-memory database for speed:
config()->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
Migrations run automatically via defineDatabaseMigrations().
Using Factories
Create Test Jobs
use Cbox\LaravelQueueMonitor\Models\JobMonitor;
// Default completed job
$job = JobMonitor::factory()->create();
// Failed job
$failed = JobMonitor::factory()->failed()->create();
// Processing job
$processing = JobMonitor::factory()->processing()->create();
// Queued job
$queued = JobMonitor::factory()->queued()->create();
// Horizon worker
$horizonJob = JobMonitor::factory()->horizon()->create();
// Slow job
$slow = JobMonitor::factory()->slow(10000)->create(); // 10 seconds
// With tags
$tagged = JobMonitor::factory()->withTags(['priority', 'email'])->create();
// Multiple jobs
$jobs = JobMonitor::factory()->count(10)->create();
Custom Attributes
$job = JobMonitor::factory()->create([
'queue' => 'emails',
'server_name' => 'web-1',
'duration_ms' => 5000,
]);
Asserting monitoring in your own app
Queue Monitor ships an InteractsWithQueueMonitor trait so your app's feature
tests can assert what was recorded without querying the tables by hand. Use it
in a test that migrates the package tables (for example with RefreshDatabase):
use Cbox\LaravelQueueMonitor\Testing\InteractsWithQueueMonitor;
uses(InteractsWithQueueMonitor::class);
it('records the invoice job', function () {
dispatch(new SendInvoice($order));
$this->assertJobRecorded(SendInvoice::class);
$this->assertJobRecorded(SendInvoice::class, times: 1);
$this->assertJobRecordedWithStatus(SendInvoice::class, 'completed');
});
Available assertions:
| Method | Asserts |
|---|---|
assertJobRecorded($class, $times = null) |
the job class was recorded (optionally an exact count) |
assertJobNotRecorded($class) |
the job class was never recorded |
assertNothingRecorded() |
nothing was recorded at all |
assertJobRecordedWithStatus($class, $status) |
a record exists with that JobStatus (enum or string) |
assertJobCompleted($class) |
a record exists with the completed status |
assertJobFailed($class) |
a record exists that failed or timed out |
recordedJobs($class = null) |
the recorded JobMonitor models as a collection |
The assertions read the real recorded state, so they work end-to-end through the package's own listeners and actions — no fake or stub to keep in sync.
Writing Tests
Unit Tests
Test isolated components:
use Cbox\LaravelQueueMonitor\Enums\JobStatus;
test('isFinished returns true for completed status', function () {
expect(JobStatus::COMPLETED->isFinished())->toBeTrue();
});
test('DTO converts to array correctly', function () {
$data = new WorkerContextData(
serverName: 'web-1',
workerId: 'worker-123',
workerType: WorkerType::QUEUE_WORK
);
$array = $data->toArray();
expect($array['server_name'])->toBe('web-1');
expect($array['worker_type'])->toBe('queue_work');
});
Feature Tests
Test integrated functionality:
test('can list jobs via API', function () {
JobMonitor::factory()->count(3)->create();
$response = $this->getJson('/api/queue-monitor/jobs');
$response->assertOk()
->assertJsonCount(3, 'data');
});
test('replay action dispatches job to queue', function () {
Queue::fake();
$job = JobMonitor::factory()->failed()->create();
$action = app(ReplayJobAction::class);
$action->execute($job->uuid);
Queue::assertPushedOn($job->queue);
});
Testing Actions
use Cbox\LaravelQueueMonitor\Actions\Core\RecordJobCompletedAction;
test('record completed action updates job status', function () {
$job = JobMonitor::factory()->processing()->create();
$event = new \Illuminate\Queue\Events\JobProcessed(
'redis',
new MockJob($job->job_id)
);
$action = app(RecordJobCompletedAction::class);
$action->execute($event);
$job->refresh();
expect($job->status)->toBe(JobStatus::COMPLETED);
expect($job->completed_at)->not->toBeNull();
expect($job->duration_ms)->toBeInt();
});
Testing Repositories
test('repository filters jobs by status', function () {
JobMonitor::factory()->count(5)->create(['status' => JobStatus::COMPLETED]);
JobMonitor::factory()->count(3)->failed()->create();
$repository = app(JobMonitorRepositoryContract::class);
$filters = new JobFilterData(statuses: [JobStatus::FAILED]);
$results = $repository->query($filters);
expect($results)->toHaveCount(3);
expect($results->every(fn($job) => $job->isFailed()))->toBeTrue();
});
Testing API Endpoints
test('statistics endpoint returns correct structure', function () {
JobMonitor::factory()->count(10)->create();
JobMonitor::factory()->count(2)->failed()->create();
$response = $this->getJson('/api/queue-monitor/statistics');
$response->assertOk()
->assertJsonStructure([
'data' => [
'total',
'completed',
'failed',
'success_rate',
],
]);
expect($response->json('data.total'))->toBe(12);
});
Custom Expectations
The package provides custom Pest expectations:
// Check if value is a specific job status
expect($job->status)->toBeJobStatus('completed');
// Check if job has any metrics
expect($job)->toHaveMetrics();
Testing Best Practices
1. Arrange-Act-Assert Pattern
test('example test', function () {
// Arrange
$job = JobMonitor::factory()->create();
// Act
$result = QueueMonitor::getJob($job->uuid);
// Assert
expect($result)->not->toBeNull();
expect($result->uuid)->toBe($job->uuid);
});
2. Use Factories
// Good
$job = JobMonitor::factory()->failed()->create();
// Avoid
$job = JobMonitor::create([
'uuid' => Str::uuid(),
'job_class' => 'App\\Jobs\\TestJob',
// ... 20 more fields
]);
3. Test Edge Cases
test('replay throws exception when payload missing', function () {
$job = JobMonitor::factory()->create(['payload' => null]);
$action = app(ReplayJobAction::class);
$action->execute($job->uuid);
})->throws(RuntimeException::class, 'payload not stored');
4. Use Pest's Higher Order Testing
test('completed jobs are finished')
->with([
JobStatus::COMPLETED,
JobStatus::FAILED,
JobStatus::TIMEOUT,
])
->expect(fn($status) => $status->isFinished())
->toBeTrue();
Mocking
Queue Facade
use Illuminate\Support\Facades\Queue;
test('replay dispatches job', function () {
Queue::fake();
$job = JobMonitor::factory()->create();
QueueMonitor::replay($job->uuid);
Queue::assertPushedOn($job->queue);
});
Events
use Illuminate\Support\Facades\Event;
test('job replay fires event', function () {
Event::fake();
$job = JobMonitor::factory()->create();
QueueMonitor::replay($job->uuid);
Event::assertDispatched(JobReplayRequested::class);
});
CI/CD Integration
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, sqlite3
- run: composer install
- run: composer test
- run: composer analyse
Coverage Requirements
Aim for:
- Overall: >90% coverage
- Actions: 100% coverage (critical business logic)
- Repositories: >95% coverage
- DTOs: 100% coverage
- API Controllers: >90% coverage
Run with coverage reporting:
composer test-coverage