# Regla: Testing — PHP / Laravel

Los tests en PHP con Laravel cubren dos dimensiones: lógica de negocio aislada
(unit tests) y comportamiento HTTP end-to-end (feature tests). Esta regla define
cuándo usar cada tipo, cómo estructurarlos y qué herramientas son válidas.

---

## Frameworks de testing

- **PHPUnit**: framework base, ya incluido en Laravel. Usar para proyectos existentes.
- **Pest**: alternativa moderna con sintaxis más concisa. Usar para proyectos nuevos
  o cuando el equipo lo adopte conscientemente.
- No mezclar PHPUnit y Pest en el mismo proyecto sin consenso del equipo.
- **Mockery**: para mocks y stubs. Integrado con ambos frameworks.

---

## Cobertura mínima: 80%

- Ejecutar con reporte de cobertura:
  ```bash
  php artisan test --coverage --min=80
  ```
- CI falla si la cobertura cae por debajo del 80%.
- La cobertura se mide sobre código de negocio: `app/Services/`, `app/Repositories/`,
  `app/Models/`. No sobre controllers ni providers.
- Cobertura del 100% con tests triviales no es el objetivo. 80% con tests
  que prueban comportamiento real vale más.

---

## Feature tests para endpoints HTTP

Los feature tests verifican el comportamiento completo del endpoint, incluyendo
middleware, validación, respuesta y efectos en la BD:

```php
class EmitirFacturaTest extends TestCase
{
    use RefreshDatabase;

    public function test_usuario_autorizado_puede_emitir_factura(): void
    {
        // Arrange
        $empresa = Empresa::factory()->create();
        $usuario = User::factory()->para($empresa)->conRol('admin')->create();
        $cliente = Cliente::factory()->para($empresa)->create();

        // Act
        $respuesta = $this->actingAs($usuario)
            ->postJson('/api/v1/facturas', [
                'cliente_id' => $cliente->id,
                'items' => [
                    ['sku' => 'PROD-01', 'qty' => 2, 'precio' => 100.00],
                ],
            ]);

        // Assert
        $respuesta->assertCreated()
            ->assertJsonPath('data.estatus', 'emitida')
            ->assertJsonPath('data.cliente.id', $cliente->id);

        $this->assertDatabaseHas('facturas', [
            'empresa_id' => $empresa->id,
            'estatus'    => 'emitida',
        ]);
    }

    public function test_usuario_sin_rol_admin_recibe_403(): void
    {
        $usuario = User::factory()->conRol('visor').create();

        $this->actingAs($usuario)
            ->postJson('/api/v1/facturas', [])
            ->assertForbidden();
    }
}
```

---

## Unit tests para lógica de negocio

Los unit tests prueban services y clases de dominio en aislamiento, mockeando
todas las dependencias externas:

```php
class CalculadoraImpuestoTest extends TestCase
{
    public function test_calcula_iva_sobre_precio_base(): void
    {
        // Arrange
        $calculadora = new CalculadoraImpuesto(tasaIva: 0.16);

        // Act
        $resultado = $calculadora->calcularIva(precio: 100.0);

        // Assert
        $this->assertEqualsWithDelta(16.0, $resultado, delta: 0.001);
    }

    public function test_lanza_excepcion_con_precio_negativo(): void
    {
        $calculadora = new CalculadoraImpuesto(tasaIva: 0.16);

        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('El precio no puede ser negativo');

        $calculadora->calcularIva(precio: -1.0);
    }
}
```

---

## Factories para datos de prueba

- NUNCA crear registros de BD con `new Model()` ni arrays hardcodeados en tests.
- Usar Laravel Model Factories. Definir estados para variaciones comunes:

```php
class FacturaFactory extends Factory
{
    public function definition(): array
    {
        return [
            'folio'         => 'FAC-' . $this->faker->unique()->numerify('#####'),
            'estatus'       => EstatusFactura::Borrador,
            'total'         => $this->faker->randomFloat(2, 100, 10000),
            'fecha_emision' => $this->faker->dateTimeBetween('-1 year', 'now'),
        ];
    }

    public function emitida(): static
    {
        return $this->state(['estatus' => EstatusFactura::Emitida]);
    }

    public function cancelada(): static
    {
        return $this->state(['estatus' => EstatusFactura::Cancelada]);
    }
}
```

---

## Database testing: RefreshDatabase vs DatabaseTransactions

| Trait | Cuándo usar | Costo |
|-------|------------|-------|
| `RefreshDatabase` | Tests que modifican el schema o necesitan estado limpio garantizado | Alto — hace migrate:fresh por clase |
| `DatabaseTransactions` | Tests CRUD simples sin cambios de schema | Bajo — rollback al final de cada test |

- Preferir `DatabaseTransactions` para tests de feature rápidos.
- Usar `RefreshDatabase` solo cuando el test necesita un estado absolutamente limpio.

---

## Mocks con Mockery

```php
public function test_emitir_factura_envia_email_al_cliente(): void
{
    // Arrange
    $mailer = Mockery::mock(MailerInterface::class);
    $mailer->shouldReceive('enviarFactura')
        ->once()
        ->with(Mockery::type(Factura::class));

    $service = new FacturaService(
        facturas: $this->app->make(FacturaRepositoryInterface::class),
        mailer: $mailer,
    );

    $factura = Factura::factory()->create();

    // Act
    $service->emitir($factura->id);

    // Assert — verificado por Mockery::shouldReceive()->once()
}
```

---

## Patrón Arrange-Act-Assert (AAA)

- Cada test tiene exactamente tres bloques separados con comentario.
- Una sola afirmación conceptual por test. Múltiples `assert*` sobre el mismo
  resultado están bien; verificar comportamientos distintos en el mismo test no.
- Nombre del test: `test_[escenario]_[resultado_esperado]`.

---

## Checklist de testing antes de merge

- [ ] `php artisan test --coverage --min=80` pasa
- [ ] Feature test para cada endpoint nuevo
- [ ] Unit test para cada método público de Service nuevo
- [ ] Factories usadas para todos los registros de BD en tests
- [ ] Sin `sleep()` ni dependencias de tiempo real (usar `Carbon::setTestNow()`)
- [ ] Patron AAA con comentarios en cada test
- [ ] Test de regresión para cada bug corregido
