# Regla: Pruebas — Rust

Las pruebas en Rust son ciudadanos de primera clase: viven junto al código que
prueban y el compilador las verifica. El sistema de tipos ya elimina clases
enteras de bugs — los tests cubren la lógica de negocio y los contratos públicos.

---

## cargo test como runner principal

- `cargo test` ejecuta todos los tests: unitarios, de integración y doctests.
- `cargo test nombre` para ejecutar un test específico.
- `cargo test -- --nocapture` para ver salida en tests que pasan.
- CI siempre ejecuta `cargo test --all-targets --all-features` antes del merge.
- Los benchmarks no corren con `cargo test` — usar `cargo bench`.

---

## rstest para tests parametrizados y fixtures

```rust
use rstest::{fixture, rstest};

#[fixture]
fn factura_valida() -> Factura {
    Factura::builder().id(FacturaId(1)).total(Monto(1500.0)).build().unwrap()
}

// Test parametrizado — evita duplicar el mismo test con distintos valores
#[rstest]
#[case(0.16, 240.0)]
#[case(0.08, 120.0)]
#[case(0.0,  0.0)]
fn test_calcular_iva_por_tasa(factura_valida: Factura, #[case] tasa: f64, #[case] esperado: f64) {
    assert_eq!(factura_valida.calcular_iva(tasa).0, esperado);
}
```

- Usar `#[fixture]` para objetos complejos reutilizados entre tests.
- Usar `#[rstest]` con `#[case]` en lugar de duplicar tests con distintos inputs.

---

## proptest para property-based testing

```rust
use proptest::prelude::*;

proptest! {
    #[test]
    fn iva_nunca_supera_el_total(total in 0.01f64..1_000_000.0) {
        let factura = FacturaFactory::con_total(total);
        prop_assert!(factura.calcular_iva(0.16).0 <= total);
    }
}
```

- Usar proptest para invariantes que deben cumplirse para cualquier input válido.
- Especialmente útil para: serialización/deserialización, algoritmos matemáticos y parsers.
- Complementa los tests unitarios — no los reemplaza.

---

## mockall para mocks de traits

```rust
use mockall::mock;

mock! {
    RepoMock {}
    impl FacturaRepo for RepoMock {
        async fn obtener(&self, id: FacturaId) -> Result<Factura, FacturaError>;
    }
}

#[tokio::test]
async fn test_servicio_propaga_error_de_repo() {
    // Arrange
    let mut repo = MockRepoMock::new();
    repo.expect_obtener()
        .returning(|_| Err(FacturaError::NoEncontrada { id: 99 }));

    // Act
    let resultado = FacturaServicio::new(Arc::new(repo))
        .obtener(FacturaId(99)).await;

    // Assert
    assert!(matches!(resultado, Err(FacturaError::NoEncontrada { .. })));
}
```

- Mockear solo dependencias externas (BD, APIs, filesystem) — nunca código propio del dominio.
- Verificar el número de llamadas con `.times(N)` para side effects esperados.

---

## cargo-llvm-cov para cobertura

```bash
cargo install cargo-llvm-cov
cargo llvm-cov --fail-under-lines 80
```

- Cobertura mínima: **80%** de líneas en módulos de lógica de negocio.
- CI debe fallar si la cobertura cae por debajo del umbral.
- Excluir código de arranque y configuración con `#[cfg(not(tarpaulin_include))]`.

---

## Tests unitarios en el mismo archivo (#[cfg(test)] mod tests)

```rust
// src/facturacion/calculo.rs
pub fn calcular_iva(base: f64, tasa: f64) -> f64 { base * tasa }

#[cfg(test)]
mod tests {
    use super::*; // accede también a funciones privadas

    #[test]
    fn iva_con_tasa_estandar() {
        // Arrange
        let (base, tasa) = (1000.0, 0.16);
        // Act
        let resultado = calcular_iva(base, tasa);
        // Assert
        assert_eq!(resultado, 160.0);
    }
}
```

- `use super::*` permite probar funciones privadas del módulo.
- `#[cfg(test)]` garantiza que el código de test no se incluye en el binario.

---

## Tests de integración en tests/

Los tests de integración viven en `tests/` en la raíz del crate y solo acceden
a la API pública:

```
mi-crate/
├── src/
└── tests/
    └── facturacion_integration.rs
```

- Usar una BD aislada por test (transacción que se revierte, o BD en memoria).
- Los tests de integración son más lentos — ejecutar en job CI separado si es necesario.

---

## Benchmarks con criterion

```rust
// benches/calculo_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn benchmark_calcular_totales(c: &mut Criterion) {
    let facturas: Vec<Factura> = (0..1000).map(FacturaFactory::nueva).collect();
    c.bench_function("calcular_totales_1000", |b| {
        b.iter(|| calcular_totales(black_box(&facturas)))
    });
}

criterion_group!(benchmarks, benchmark_calcular_totales);
criterion_main!(benchmarks);
```

- Los benchmarks van en `benches/` — no en `src/` ni `tests/`.
- `black_box` previene que el compilador optimice el código de benchmark.
- No corren en CI normal — solo al investigar regresiones de performance.

---

## Patrón Arrange-Act-Assert

- Los tres bloques siempre separados con comentario o línea en blanco.
- Nombre del test: `test_[unidad]_[condicion]_[resultado_esperado]`.
- Un solo concepto por test. Si el Arrange es extenso, extraer a fixture de rstest.

---

## Checklist de pruebas antes de hacer merge

- [ ] `cargo test --all-targets` pasa completamente
- [ ] Tests nuevos para toda lógica de negocio nueva
- [ ] Test de regresión escrito antes del fix para todo bug
- [ ] `cargo llvm-cov --fail-under-lines 80` pasa
- [ ] Tests unitarios en `#[cfg(test)] mod tests` del mismo archivo
- [ ] Tests de integración en `tests/`
- [ ] Sin `time::sleep` en tests — usar mocks del reloj
- [ ] Tests parametrizados con `rstest` en lugar de tests duplicados
