QueueFake en Laravel: Testing de Colas sin Dependencias
Introducción
Uno de los desafíos más comunes al escribir tests en Laravel es validar que tus jobs se despachen correctamente sin ejecutar Redis o la infraestructura de colas en el entorno de pruebas. El problema es obvio: testear colas en producción es lento, frágil y requiere dependencias externas que no siempre están disponibles en tu entorno de CI/CD.
QueueFake es la solución elegante que Laravel proporciona para interceptar y validar el comportamiento de tus colas sin necesidad de ejecutar ningún driver real. A través de QueueFake puedes:
- Verificar que un job fue despachado
- Validar los datos pasados al job
- Probar el comportamiento de reintentos y fallos
- Simular comportamientos específicos de colas
- Testear con métodos
beforePushingyafterPushing
En este artículo exploraremos las capacidades avanzadas de QueueFake, introducidas y mejoradas en las últimas versiones de Laravel, con ejemplos prácticos que puedes implementar inmediatamente.
¿Qué es QueueFake?
QueueFake es una implementación fake del gestor de colas de Laravel que reemplaza el driver real durante tests. En lugar de enviar jobs a Redis o a una cola real, QueueFake los almacena en memoria y te proporciona métodos de aserción para verificar que se comportaron como esperabas.
Para usarlo, simplemente llama a Queue::fake() al inicio de tu test:
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
use App\Jobs\SendEmailJob;
class QueueTest extends TestCase
{
public function test_job_is_dispatched()
{
Queue::fake();
// Tu código que dispecha un job
SendEmailJob::dispatch('user@example.com');
// Verificar que el job fue despachado
Queue::assertPushed(SendEmailJob::class);
}
}
Cuando activas QueueFake, Laravel intercepta todas las operaciones de cola y las maneja en memoria. Esto significa que los jobs nunca se ejecutan realmente—solo se registran sus despachos para que los valides después.
Métodos Esenciales de QueueFake
assertPushed: Verificar que un Job fue Despachado
El método más fundamental es assertPushed(). Te permite confirmar que un job específico fue añadido a la cola:
public function test_notification_job_is_pushed()
{
Queue::fake();
// Trigger code que dispecha el job
$this->post('/api/users', [
'name' => 'Juan',
'email' => 'juan@example.com'
]);
// Verificar que el job se despachó
Queue::assertPushed(SendWelcomeEmailJob::class);
}
Puedes ser más específico con un callback que valide los datos:
public function test_job_dispatched_with_correct_data()
{
Queue::fake();
SendEmailJob::dispatch('user@example.com', 'Welcome!');
Queue::assertPushed(SendEmailJob::class, function ($job) {
return $job->email === 'user@example.com' &&
$job->subject === 'Welcome!';
});
}
assertNotPushed: Verificar que un Job NO fue Despachado
Útil para validar lógica condicional:
public function test_job_not_pushed_for_inactive_users()
{
Queue::fake();
$inactiveUser = User::factory()->inactive()->create();
// Trigger code
$inactiveUser->sendNotification();
// Verificar que el job NO se despachó
Queue::assertNotPushed(SendNotificationJob::class);
}
assertPushedOn: Validar la Cola Específica
Si trabajas con múltiples colas (default, emails, heavy-processing), puedes validar en cuál se despachó:
public function test_heavy_job_uses_correct_queue()
{
Queue::fake();
ProcessVideoJob::dispatch($video->id);
// Verificar que se envió a la cola "processing"
Queue::assertPushedOn('processing', ProcessVideoJob::class);
}
assertPushedWithChain: Validar Cadenas de Jobs
Los jobs encadenados son comunes en Laravel. Puedes validarlos así:
public function test_job_chain_is_correct()
{
Queue::fake();
Bus::chain([
new ProcessPayment($order),
new SendInvoice($order),
new UpdateInventory($order)
])->dispatch();
Queue::assertPushedWithChain(ProcessPayment::class, [
SendInvoice::class,
UpdateInventory::class,
]);
}
assertPushedTimes: Contar Despachos
Verifica cuántas veces se despachó un job:
public function test_job_dispatched_for_each_user()
{
Queue::fake();
$users = User::factory(5)->create();
$users->each(fn($user) =>
SendBirthdayEmailJob::dispatch($user)
);
// Verificar que se despachó 5 veces
Queue::assertPushedTimes(SendBirthdayEmailJob::class, 5);
}
beforePushing y afterPushing: Hooks Avanzados
Las versiones recientes de Laravel introdujeron beforePushing y afterPushing en QueueFake, permitiéndote ejecutar código antes y después de que un job sea despachado.
beforePushing: Interceptar Antes del Despacho
Útil para modificar el comportamiento o validar precondiciones:
public function test_job_payload_is_modified()
{
Queue::fake();
Queue::beforePushing(SendEmailJob::class, function ($job) {
// Validar que el email tenga un formato válido
if (!filter_var($job->email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
// Podrías log el job antes de ser despachado
Log::info('Job being pushed', ['job' => get_class($job)]);
});
SendEmailJob::dispatch('invalid-email');
// Esto lanzará una excepción
}
afterPushing: Ejecutar Después del Despacho
Útil para limpiar, auditar o generar side effects:
public function test_job_audit_is_logged()
{
Queue::fake();
$jobsDispatched = [];
Queue::afterPushing(SendEmailJob::class, function ($job) use (&$jobsDispatched) {
$jobsDispatched[] = [
'class' => get_class($job),
'email' => $job->email,
'timestamp' => now()
];
});
SendEmailJob::dispatch('user1@example.com');
SendEmailJob::dispatch('user2@example.com');
$this->assertCount(2, $jobsDispatched);
}
Caso de Uso Práctico: Testing de un Workflow Completo
Supongamos que tienes un sistema donde crear una orden dispecha múltiples jobs. Así es cómo testearlo completamente:
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Bus;
use Tests\TestCase;
use App\Jobs\ProcessPayment;
use App\Jobs\SendInvoice;
use App\Jobs\UpdateInventory;
use App\Models\Order;
class OrderCreationTest extends TestCase
{
public function test_creating_order_dispatches_correct_jobs()
{
Queue::fake();
// Crear una orden vía API
$response = $this->post('/api/orders', [
'items' => [
['product_id' => 1, 'quantity' => 2],
],
'payment_method' => 'card'
]);
$response->assertStatus(201);
$order = Order::latest()->first();
// Validar que se despachó el job de pago
Queue::assertPushed(ProcessPayment::class, function ($job) use ($order) {
return $job->order_id === $order->id;
});
// Validar que se despachó el job de factura
Queue::assertPushed(SendInvoice::class, function ($job) use ($order) {
return $job->order_id === $order->id;
});
// Validar que se actualizó el inventario
Queue::assertPushed(UpdateInventory::class);
// Verificar que se despacharon exactamente 3 jobs
Queue::assertPushedTimes(ProcessPayment::class, 1);
Queue::assertPushedTimes(SendInvoice::class, 1);
Queue::assertPushedTimes(UpdateInventory::class, 1);
}
public function test_invalid_payment_method_does_not_process_payment()
{
Queue::fake();
$response = $this->post('/api/orders', [
'items' => [['product_id' => 1, 'quantity' => 1]],
'payment_method' => 'invalid'
]);
$response->assertStatus(422);
// ProcessPayment no debería haberse despachado
Queue::assertNotPushed(ProcessPayment::class);
}
public function test_tracking_all_dispatched_jobs()
{
Queue::fake();
$allJobs = [];
Queue::afterPushing(function ($job) use (&$allJobs) {
$allJobs[] = [
'class' => get_class($job),
'timestamp' => now()
];
});
// Crear múltiples órdenes
for ($i = 0; $i < 5; $i++) {
$this->post('/api/orders', [
'items' => [['product_id' => 1, 'quantity' => 1]],
'payment_method' => 'card'
]);
}
// Debería haber despachado 15 jobs (3 por orden × 5 órdenes)
$this->assertCount(15, $allJobs);
// Validar que cada orden generó los 3 jobs
$paymentJobs = array_filter($allJobs, fn($j) =>
str_contains($j['class'], 'ProcessPayment')
);
$this->assertCount(5, $paymentJobs);
}
}
Integración con Bus Fake
Para testing más completo, combina QueueFake con Bus::fake(), que además testea el despachador de comandos/jobs:
public function test_order_flow_with_bus_fake()
{
Bus::fake();
// Dispatch un command que a su vez dispecha jobs
$this->dispatch(new CreateOrderCommand($orderData));
// Validar el comando
Bus::assertDispatched(CreateOrderCommand::class);
// Validar los jobs
Bus::assertDispatched(ProcessPayment::class);
Bus::assertDispatched(SendInvoice::class);
}
Mejores Prácticas
1. Testea Comportamiento, No Implementación
Enfócate en validar que los jobs correctos se despachen con datos correctos, no en detalles internos:
// ✅ Bien: Validar el resultado funcional
Queue::assertPushed(SendEmailJob::class, function ($job) {
return $job->email === 'user@example.com';
});
// ❌ Evitar: Testear detalles de implementación
Queue::assertPushed(SendEmailJob::class, function ($job) {
return $job->delay === 60 && $job->tries === 3;
});
2. Usa Factories en Tests
Genera datos realistas para tests más robustos:
public function test_job_dispatched_for_each_active_user()
{
Queue::fake();
User::factory(10)->active()->create();
$this->artisan('users:send-newsletter');
Queue::assertPushedTimes(SendNewsletterJob::class, 10);
}
3. Limpia el Estado entre Tests
Aunque Laravel resetea la cola entre tests automáticamente, sé explícito:
public function setUp(): void
{
parent::setUp();
Queue::fake();
}
Puntos Clave
- QueueFake permite testear jobs sin ejecutar Redis ni infraestructura de colas reales
assertPushed()verifica que un job fue despachado; acepta callbacks para validar datosassertNotPushed()confirma que un job NO se despachó (útil para lógica condicional)assertPushedOn()valida que el job se envió a la cola correctabeforePushing()yafterPushing()permiten interceptar jobs antes y después de despachoassertPushedWithChain()valida cadenas de jobs en secuenciaassertPushedTimes()cuenta cuántas veces se despachó un job específico- Combina con
Bus::fake()para testing más completo del flujo de comandos - Tests más rápidos y confiables que testing de colas reales sin dependencias externas