Kolejki to jedyny fragment aplikacji, którego nie widać w testach HTTP — jeśli nie sprawdzisz, że żądanie zakolejkowało właściwe zadanie z właściwym payloadem, literówka w nazwie klasy albo brakujący dispatch() przejdą cały CI i wyjdą na produkcji. Laravel daje na to dwie fasady: Queue::fake() i Bus::fake(). Ten poradnik pokazuje kompletny zestaw asercji, testowanie logiki samych zadań i pułapki, które sprawiają, że testy kolejek kłamią.
Najważniejsze: Najważniejsze —
Queue::fake()w teście feature sprawdza dyspozycję (że zadanie zostało zakolejkowane i z jakim payloadem), nie wykonuje zadania. Logikę zadania testujesz osobno: bezpośrednim wywołaniemhandle()albo testem z prawdziwym sterownikiemsync.Bus::fake()działa analogicznie dla batchy i łańcuchów.
Dwie warstwy testowania kolejek
Testy kolejek dzielą się na dwie warstwy i mieszanie ich to najczęstszy błąd
- Dyspozycja — czy aplikacja we właściwym momencie kolejkuje właściwe zadanie? Testujesz
Queue::fake()+ asercje. Szybkie, nie dotykają kolejki. - Wykonanie — czy zadanie robi to, co powinno? Testujesz bezpośrednio logikę zadania (wywołanie
handle()na obiekcie), bez fasad i bez kolejki.
Test, który próbuje robić obie rzeczy naraz (fake + oczekiwanie efektów), zawsze kłamie: Queue::fake() sprawia, że zadanie nigdy się nie wykona, więc żadne efekty nie powstaną.
Queue::fake — asercje dyspozycji
- Podstawowy test feature: żądanie HTTP ma zakolejkować zadanie synchronizacji.
use App\Jobs\SyncOrderToErp;
use Illuminate\Support\Facades\Queue;
public function test_order_creation_dispatches_sync_job(): void
{
Queue::fake();
$this->post('/orders', [
'product_id' => 1,
'quantity' => 2,
])->assertRedirect();
Queue::assertPushed(SyncOrderToErp::class);
}
Kompletny zestaw asercji, które przydają się w praktyce
// Zadanie zakolejkowane z konkretnym payloadem
Queue::assertPushed(SyncOrderToErp::class, function ($job) {
return $job->order->id === 42;
});
// Zadanie trafiło do właściwej kolejki (priorytet)
Queue::assertPushedOn('high', SendInvoiceEmail::class);
// Zadanie NIE zostało zakolejkowane (np. walidacja biznesowa)
Queue::assertNotPushed(SyncOrderToErp::class);
// Dokładnie N zadań danego typu
Queue::assertPushed(SyncOrderToErp::class, 1);
// Nic nie poszło do kolejki
Queue::assertNothingPushed();
Warto testować payload, nie tylko samą klasę — $job->order->id === 42 łapie błędy, w których zadanie jest kolejkowane z pustym albo złym obiektem. Dwa testy: „zakolejkowano" i „zakolejkowano z właściwymi danymi" kosztują sekundy, a oszczędzają debugowania na produkcji.
Bus::fake — batche i łańcuchy
Gdy aplikacja używa Bus::batch() albo Bus::chain(), Queue::fake() ich nie obejmuje — potrzebujesz Bus::fake()
use App\Jobs\ImportRow;
use Illuminate\Support\Facades\Bus;
public function test_import_dispatches_batch(): void
{
Bus::fake();
$this->post('/imports', ['file' => $file])->assertRedirect();
Bus::assertBatched(function ($batch) {
return $batch->name === 'Import produktów'
&& $batch->jobs->count() === 100;
});
}
Dostępne asercje dla batchy i łańcuchów
Bus::assertBatched(fn ($batch) => ...)— batch zakolejkowany z warunkiem (nazwa, liczba zadań, kolejka).Bus::assertChained([FetchInvoice::class, GenerateInvoicePdf::class])— łańcuch w dokładnie takiej kolejności.Bus::assertDispatched(SyncPrices::class)— pojedyncze zadanie przezBus::dispatch().Bus::assertNothingBatched()— nic nie poszło jako batch.
Testowanie logiki samego zadania
Właściwą pracę zadania testujesz bez kolejki — najprościej wywołaniem handle() na przygotowanym obiekcie
public function test_job_marks_order_as_synced(): void
{
$order = Order::factory()->create(['erp_id' => null]);
$erp = Mockery::mock(ErpClient::class);
$erp->shouldReceive('push')->once()->andReturn('ERP-123');
(new SyncOrderToErp($order))->handle($erp);
$this->assertSame('ERP-123', $order->fresh()->erp_id);
}
Tak testujesz pełną logikę — łącznie z wyjątkami, retry i idempotencją — bez dotykania kolejki i bez Queue::fake(). Dwie zalety: test jest szybki (brak bootstrapu workera) i sprawdza efekty, a nie tylko wywołanie.
Do testów integracyjnych z prawdziwym cyklem kolejki użyj sterownika sync — zadanie wykona się natychmiast, w tym samym procesie:
// phpunit.xml: <env name="QUEUE_CONNECTION" value="sync"/>
public function test_full_flow_with_sync_queue(): void
{
$this->post('/orders', [...])->assertRedirect();
$this->assertDatabaseHas('orders', ['erp_id' => 'ERP-123']);
}
Wskazówka: Trzy poziomy testów kolejek: (1)
Queue::fake()+ asercje dyspozycji — najtańsze, najszersze; (2) testhandle()z mockami — logika zadania; (3) test ze sterownikiemsync— pełny przepływ end-to-end w procesie. Nie każdy projekt potrzebuje wszystkich trzech, ale warstwa 1 powinna istnieć zawsze.
Faking wybranych zadań
Czasem chcesz, żeby jedno zadanie wykonało się naprawdę, a inne były sfakowane — np. testujesz wysyłkę maila, ale nie chcesz realnej synchronizacji z ERP:
Queue::fake([SyncOrderToErp::class]); // fake tylko tego zadania
Queue::fake()->except([SendInvoiceEmail::class]); // wszystko oprócz tego
Pierwszy wariant przydaje się, gdy przechodzisz z synchronicznego wykonania na kolejki — możesz sfakować tylko nowe zadania, a resztę zostawić działającą.
Pułapki, które sprawiają, że testy kłamią
Queue::fake()+ oczekiwanie efektów — zadanie nigdy się nie wykona; jeśli test „przechodzi", sprawdza coś innego, niż myślisz.- Fake bez asercji —
Queue::fake()bezassertPushed()nie testuje niczego. Fake bez asercji to fałszywe poczucie bezpieczeństwa. QUEUE_CONNECTION=syncw testach bez świadomości — wtedyQueue::fake()nie jest potrzebne, ale zadania wykonują się w procesie; błędy w zadaniach wywalają testy HTTP (często pożądane, ale trzeba o tym wiedzieć).- Testowanie prywatnych metod zadania — testuj
handle(), nie implementację. Refaktoryzacja wewnątrz zadania nie powinna łamać testów. - Brak testu retry — sprawdź, że zadanie po wyjątku faktycznie wraca do kolejki (test z
expectExceptioni ponownymhandle()), bo literówka w$triesteż jest błędem.
Pest czy PHPUnit?
Oba działają identycznie — Queue::fake() to fasada, nie zależy od frameworka testowego. W Pesta zapis jest zwięźlejszy
it('kolejkuje synchronizację po utworzeniu zamówienia', function () {
Queue::fake();
$this->post('/orders', ['product_id' => 1])->assertRedirect();
Queue::assertPushed(SyncOrderToErp::class);
});
Wybór między Pest a PHPUnit opisuję w kontekście całego projektu — oba podejścia są w użyciu produkcyjnym i nie ma tu złej decyzji. Ważne, żeby warstwa dyspozycji była pokryta w CI razem z resztą testów.
Najczęściej zadawane pytania
Czym różni się Queue::fake() od Bus::fake()?
Queue::fake() przechwytuje klasyczne kolejkowanie (dispatch(), Queue::push()) — pokrywa większość przypadków. Bus::fake() obsługuje batch i łańcuchy (Bus::batch(), Bus::chain()), których Queue::fake() nie przechwytuje. W nowoczesnych aplikacjach używa się obu, zależnie od tego, czego dotyczy test.
Czy Queue::fake() wykonuje zadania?
Nie — fake podmienia sterownik kolejki na atrapę, która tylko zapisuje, co zostało zakolejkowane. Zadania nie wykonują się w ogóle. Dlatego efekty zadania testujesz osobno (bezpośrednie handle() albo sterownik sync), a fake służy wyłącznie do asercji dyspozycji.
Jak przetestować, że zadanie NIE zostało zakolejkowane?
Queue::assertNotPushed(SyncOrderToErp::class) po wykonaniu żądania. Scenariusz: walidacja biznesowa blokuje operację (np. zamówienie bez wymaganego pola), więc zadanie nie powinno trafić do kolejki. Warto też sprawdzić Queue::assertNothingPushed() dla całkowicie zablokowanych ścieżek.
Jak testować zadania z retry i backoffem?
Testuj logikę przez handle(): przygotuj stan, w którym pierwsze wywołanie rzuca wyjątek, i sprawdź, że stan po nim jest spójny (idempotencja). Konfigurację retry ($tries, $backoff) sprawdzisz asercją na właściwościach zadania — $this->assertSame(3, (new SyncOrderToErp($order))->tries) — albo w teście integracyjnym ze sterownikiem sync.
Czy testy kolejek działają z Laravel Horizon?
Horizon jest warstwą nad kolejką — nie wpływa na Queue::fake(). Testy działają identycznie, niezależnie od tego, czy produkcja używa Horizon, Supervisora czy sterownika database. Sam panel Horizon testujesz osobno (dostęp, autoryzacja przez bramkę viewHorizon), a nie przez fake.
👉 Zadania znikają bez śladu, a testy „przechodzą"? Prawdopodobnie warstwa dyspozycji nie jest pokryta. Opisz projekt na kontakt@informatix.com.pl lub przez formularz — dopiszemy testy kolejek do istniejącego pipeline'u.
📖 Zobacz również: Kolejki w Laravel i Laravel Horizon — poradnik, queue:work vs queue:listen, job batching, chains i rate limiting oraz usługa Laravel development.