Skip to content

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