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,
]);

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