Appearance
Testing
◄ Anterior: Multi-Tenant | Índice | Siguiente: Troubleshooting ►
Tabla de Contenidos
Tests Unitarios (PHPUnit con Mocks)
JobDispatcherTest
Archivo: Tests/Unit/Core/JobDispatcherTest.php
Propósito: Verificar que JobDispatcher valida límites, crea jobs y lanza workers
Tests a implementar:
php
class JobDispatcherTest extends TestCase
{
private JobDispatcher $dispatcher;
private JobRepository $mockRepo;
protected function setUp(): void
{
$this->mockRepo = $this->createMock(JobRepository::class);
$this->dispatcher = new JobDispatcher($this->mockRepo);
}
public function testDispatchCreatesJobSuccessfully(): void
{
// Arrange
$this->mockRepo->expects($this->once())
->method('countPendingByUser')
->with(1)
->willReturn(5); // Bajo el límite
$this->mockRepo->expects($this->once())
->method('create')
->willReturn(123); // Job ID
// Act
$jobId = $this->dispatcher->dispatch(
'batch_invoicing',
['test' => 'data'],
1,
'suc0001'
);
// Assert
$this->assertEquals(123, $jobId);
}
public function testDispatchThrowsExceptionWhenTooManyJobs(): void
{
// Arrange
$this->mockRepo->expects($this->once())
->method('countPendingByUser')
->with(1)
->willReturn(10); // En el límite
// Assert
$this->expectException(TooManyJobsException::class);
// Act
$this->dispatcher->dispatch(
'batch_invoicing',
['test' => 'data'],
1,
'suc0001'
);
}
}JobExecutorTest
Archivo: Tests/Unit/Core/JobExecutorTest.php
Propósito: Verificar que JobExecutor ejecuta handlers, actualiza estado y crea notificaciones
Tests a implementar:
php
class JobExecutorTest extends TestCase
{
private JobExecutor $executor;
private JobRepository $mockRepo;
private NotificationRepository $mockNotifRepo;
private JobHandlerInterface $mockHandler;
protected function setUp(): void
{
$this->mockRepo = $this->createMock(JobRepository::class);
$this->mockNotifRepo = $this->createMock(NotificationRepository::class);
$this->mockHandler = $this->createMock(JobHandlerInterface::class);
$this->executor = new JobExecutor(
$this->mockRepo,
$this->mockNotifRepo,
$this->createMock(ConnectionManager::class)
);
$this->mockHandler->method('getType')->willReturn('test_job');
$this->executor->registerHandler($this->mockHandler);
}
public function testExecuteUpdatesJobToCompletedOnSuccess(): void
{
// Arrange
$job = new BackgroundJob(
id: 123,
type: 'test_job',
status: 'pending',
payload: ['test' => 'data'],
user_id: 1,
schema: 'suc0001'
);
$this->mockRepo->expects($this->once())
->method('findById')
->with(123)
->willReturn($job);
$this->mockHandler->expects($this->once())
->method('handle')
->with(['test' => 'data'])
->willReturn(['success' => true]);
$this->mockRepo->expects($this->once())
->method('update')
->with($this->callback(function ($job) {
return $job->status === 'completed'
&& $job->result === ['success' => true];
}));
// Act
$this->executor->execute(123);
}
public function testExecuteUpdatesJobToFailedOnException(): void
{
// Arrange
$job = new BackgroundJob(
id: 123,
type: 'test_job',
status: 'pending',
payload: ['test' => 'data'],
user_id: 1,
schema: 'suc0001'
);
$this->mockRepo->method('findById')->willReturn($job);
$this->mockHandler->expects($this->once())
->method('handle')
->willThrowException(new Exception('Test error'));
$this->mockRepo->expects($this->once())
->method('update')
->with($this->callback(function ($job) {
return $job->status === 'failed'
&& $job->error === 'Test error';
}));
// Act
$this->executor->execute(123);
}
}BatchInvoicingJobHandlerTest
Archivo: Tests/Unit/Ventas/BatchInvoicingJobHandlerTest.php
Propósito: Verificar que handler procesa batch correctamente y acumula errores
Tests a implementar:
php
class BatchInvoicingJobHandlerTest extends TestCase
{
private BatchInvoicingJobHandler $handler;
private FacturaService $mockService;
protected function setUp(): void
{
$this->mockService = $this->createMock(FacturaService::class);
$this->handler = new BatchInvoicingJobHandler($this->mockService);
}
public function testHandleProcessesAllClientes(): void
{
// Arrange
$payload = [
'cliente_ids' => [1, 2, 3],
'fecha' => '2026-02-05',
'concepto' => 'Test',
'monto_base' => 1000
];
$this->mockService->expects($this->exactly(3))
->method('insert')
->willReturnOnConsecutiveCalls(
new FacturaDTO(id: 101),
new FacturaDTO(id: 102),
new FacturaDTO(id: 103)
);
// Act
$result = $this->handler->handle($payload);
// Assert
$this->assertEquals(3, $result['facturas_creadas']);
$this->assertEquals([101, 102, 103], $result['factura_ids']);
$this->assertEmpty($result['errores']);
}
public function testHandleAccumulatesErrorsWithoutFailingBatch(): void
{
// Arrange
$payload = [
'cliente_ids' => [1, 2],
'fecha' => '2026-02-05',
'concepto' => 'Test',
'monto_base' => 1000
];
$this->mockService->expects($this->exactly(2))
->method('insert')
->willReturnCallback(function ($dto) {
if ($dto->cliente_id === 1) {
return new FacturaDTO(id: 101);
}
throw new Exception('Cliente 2 no encontrado');
});
// Act
$result = $this->handler->handle($payload);
// Assert
$this->assertEquals(1, $result['facturas_creadas']);
$this->assertCount(1, $result['errores']);
$this->assertEquals(2, $result['errores'][0]['cliente_id']);
}
}Tests de Integración (PHPUnit con DB Real)
BackgroundJobsFlowTest
Archivo: Tests/Integration/Core/BackgroundJobsFlowTest.php
Propósito: Testear flujo completo end-to-end con base de datos real
Setup:
php
class BackgroundJobsFlowTest extends BaseIntegrationTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setupDatabase();
}
}Tests a implementar:
Test: Flujo Completo Dispatch → Execute → Complete
php
public function testCompleteFlowFromDispatchToCompletion(): void
{
// Arrange: Registrar handler de prueba
$testHandler = new class implements JobHandlerInterface {
public function getType(): string { return 'test_job'; }
public function handle(array $payload): array {
return ['processed' => $payload['value'] * 2];
}
};
$this->executor->registerHandler($testHandler);
// Act: Despachar job
$jobId = $this->dispatcher->dispatch(
'test_job',
['value' => 5],
$userId = 1,
$schema = 'suc0001'
);
// Simular ejecución del worker
$this->executor->execute($jobId);
// Assert: Verificar en DB
$job = $this->jobRepo->findById($jobId);
$this->assertNotNull($job);
$this->assertEquals('completed', $job->status);
$this->assertEquals(['processed' => 10], $job->result);
$this->assertNotNull($job->completed_at);
// Assert: Verificar notificación creada
$notifications = $this->notificationRepo->findUnreadByUser($userId);
$this->assertCount(1, $notifications);
$this->assertEquals('success', $notifications[0]->type);
}Test: Aislamiento Multi-Tenant
php
public function testMultiTenantIsolation(): void
{
// Arrange: Crear datos en dos schemas
$this->setupSchema('suc0001');
$cliente1 = $this->createTestCliente(['nombre' => 'Cliente 1']);
$this->setupSchema('suc0002');
$cliente2 = $this->createTestCliente(['nombre' => 'Cliente 2']);
// Act: Despachar job en suc0001
$jobId = $this->dispatcher->dispatch(
'batch_invoicing',
['cliente_ids' => [$cliente1->id, $cliente2->id]],
1,
'suc0001'
);
$this->executor->execute($jobId);
// Assert: Solo cliente1 debe procesarse
$this->setupSchema('suc0001');
$facturas = $this->getFacturasCreadas();
$this->assertCount(1, $facturas);
// Assert: NO debe haber facturas en suc0002
$this->setupSchema('suc0002');
$facturas = $this->getFacturasCreadas();
$this->assertCount(0, $facturas);
}Test: Job Failure Crea Notificación de Error
php
public function testJobFailureCreatesErrorNotification(): void
{
// Arrange: Handler que siempre falla
$failingHandler = new class implements JobHandlerInterface {
public function getType(): string { return 'failing_job'; }
public function handle(array $payload): array {
throw new Exception('Expected test failure');
}
};
$this->executor->registerHandler($failingHandler);
// Act
$jobId = $this->dispatcher->dispatch(
'failing_job',
['test' => 'data'],
$userId = 1,
'suc0001'
);
$this->executor->execute($jobId);
// Assert: Job debe estar failed
$job = $this->jobRepo->findById($jobId);
$this->assertEquals('failed', $job->status);
$this->assertStringContainsString('Expected test failure', $job->error);
// Assert: Notificación tipo error
$notifications = $this->notificationRepo->findUnreadByUser($userId);
$this->assertCount(1, $notifications);
$this->assertEquals('error', $notifications[0]->type);
}Cobertura Esperada
Unit Tests
Objetivo: > 80% de cobertura
Componentes cubiertos:
- JobDispatcher (100%)
- JobExecutor (100%)
- JobRepository (100%)
- NotificationRepository (100%)
- Handlers individuales (100%)
Integration Tests
Objetivo: 100% de flujos críticos
Flujos obligatorios:
- ✅ Dispatch → Execute → Complete
- ✅ Dispatch → Execute → Fail
- ✅ Multi-tenant isolation
- ✅ DOS protection (límite de jobs)
- ✅ Notification creation
- ✅ Error handling
Comandos de Testing
bash
# Ejecutar solo tests unitarios
vendor/bin/phpunit --testsuite=unit
# Ejecutar solo tests de integración
vendor/bin/phpunit --testsuite=integration
# Ejecutar tests con cobertura
vendor/bin/phpunit --coverage-html coverage
# Ejecutar tests de un componente específico
vendor/bin/phpunit Tests/Unit/Core/JobDispatcherTest.php◄ Anterior: Multi-Tenant | Índice | Siguiente: Troubleshooting ►