Skip to content

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 ►