---
name: rust-testing
description: >
  Testing Rust con rstest, proptest, mockall y cargo-llvm-cov. Cubre tests
  inline con #[cfg(test)], async tests con tokio::test, integration tests
  y doc tests. Cargar cuando se escriban tests Rust o se configure cobertura.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
exclusiones:
  - "No cargar para implementar el código Rust de producción — primero implementar con `rust-experto`, luego escribir tests."
  - "No cargar para errores de compilación Rust en los tests — para build errors cargar `build-errors-rust`."
  - "No cargar para benchmarks avanzados con criterion o divan — esas herramientas requieren configuración específica no cubierta en este skill de testing estándar."
  - "No cargar para tests de otros lenguajes — este skill cubre el ecosistema de testing de Rust específicamente."
evolvable: true  # default para skill estandar
---
# Rust Testing — rstest, proptest y Cobertura

Cubre el ecosistema de testing Rust: tests inline, rstest para fixtures y
parametrizados, proptest para property-based testing, mockall para mocks de
traits, async tests con tokio::test y cargo-llvm-cov para cobertura.

## Cuándo cargar este skill

Invoca `Skill("rust-testing")` cuando:

- Se escriban tests unitarios o de integración en Rust
- Se use rstest para fixtures compartidos o tests parametrizados
- Se implemente property-based testing con proptest
- Se mocken traits con mockall
- Se configure cobertura con cargo-llvm-cov

## Cuándo NO cargar

- El código Rust de producción aún no está implementado — primero implementar con `rust-experto`, luego escribir tests.
- Los errores son de compilación Rust en los archivos de test — cargar `build-errors-rust`.
- La pregunta es sobre benchmarks avanzados con criterion o divan — esas herramientas tienen su propio flujo de configuración no cubierto en este skill.
- Los tests son de otro lenguaje — este skill cubre el ecosistema de testing específico de Rust.

## Conceptos clave

### Tests inline con #[cfg(test)]

El módulo `#[cfg(test)]` en el mismo archivo que el código bajo prueba tiene
acceso a items privados. Es el lugar correcto para tests unitarios de implementación.
Los integration tests en `tests/` prueban solo la API pública del crate.

### rstest

`rstest` permite fixtures inyectados automáticamente y casos de prueba declarativos
con `#[rstest]` y `#[case(...)]`. Reduce el boilerplate de setup compartido.

### proptest

Genera inputs aleatorios que satisfacen una estrategia y ejecuta el test cientos
de veces. Encoge (shrinks) el input fallido al caso más pequeño que reproduce el fallo.

## Reglas obligatorias

### assert_eq! con mensaje — facilita debugging

Siempre agregar un mensaje de contexto en assertions no triviales:
`assert_eq!(got, want, "al calcular descuento con precio={}", precio)`

### #[tokio::test] para tests async — no block_on manual

```rust
// MAL
#[test]
fn test_crear_pedido() {
    let result = tokio::runtime::Runtime::new()
        .unwrap()
        .block_on(crear_pedido(req));
}

// BIEN
#[tokio::test]
async fn test_crear_pedido() {
    let result = crear_pedido(req).await;
    assert!(result.is_ok());
}
```

### Integration tests usan solo API pública del crate

Los tests en `tests/` no importan módulos internos con `use mi_crate::internal::`.
Si necesitas acceder a internos, el test debe estar en un módulo `#[cfg(test)]`.

### mockall solo en el módulo de test — no en producción

Los mocks generados por `#[automock]` aumentan el binario. El atributo
`#[cfg_attr(test, automock)]` aplica la macro solo al compilar tests.

## Patrones recomendados

### Test unitario inline con acceso a privados

```rust
// src/pedido.rs
pub struct Pedido {
    id: Uuid,
    estado: Estado,
    items: Vec<Item>,
}

impl Pedido {
    fn calcular_subtotal(&self) -> Decimal { /* ... */ }
}

#[cfg(test)]
mod tests {
    use super::*;  // accede a calcular_subtotal aunque sea privada

    #[test]
    fn subtotal_suma_precio_por_cantidad() {
        let pedido = Pedido {
            id: Uuid::new_v4(),
            estado: Estado::Borrador,
            items: vec![
                Item::new(Decimal::new(100, 0), 2),
                Item::new(Decimal::new(50, 0), 1),
            ],
        };
        assert_eq!(
            pedido.calcular_subtotal(),
            Decimal::new(250, 0),
            "subtotal debe ser suma de precio*cantidad"
        );
    }
}
```

### rstest con fixtures y casos parametrizados

```rust
use rstest::*;

#[fixture]
fn pedido_borrador() -> Pedido {
    Pedido::nuevo(ClienteId::new_v4())
}

#[fixture]
fn pedido_confirmado(pedido_borrador: Pedido) -> Pedido {
    pedido_borrador.confirmar().unwrap()
}

#[rstest]
#[case(Estado::Borrador, true)]
#[case(Estado::Confirmado, false)]
#[case(Estado::Cancelado, false)]
fn puede_cancelar_solo_en_estado_borrador(
    #[case] estado: Estado,
    #[case] esperado: bool,
    pedido_borrador: Pedido,
) {
    let pedido = pedido_borrador.con_estado(estado);
    assert_eq!(pedido.puede_cancelar(), esperado);
}
```

### proptest para invariantes de dominio

```rust
use proptest::prelude::*;

proptest! {
    #[test]
    fn descuento_nunca_supera_precio(
        precio in 0.01f64..10_000.0,
        porcentaje in 0u8..=100,
    ) {
        let descuento = calcular_descuento(precio, porcentaje);
        prop_assert!(
            descuento <= precio,
            "descuento {} supero precio {}", descuento, precio
        );
        prop_assert!(descuento >= 0.0);
    }
}
```

### mockall para traits

```rust
use mockall::automock;

#[cfg_attr(test, automock)]
pub trait PedidoRepository: Send + Sync {
    async fn obtener(&self, id: Uuid) -> Result<Pedido, AppError>;
    async fn guardar(&self, pedido: &Pedido) -> Result<(), AppError>;
}

#[cfg(test)]
mod tests {
    use super::*;
    use mockall::predicate::*;

    #[tokio::test]
    async fn service_retorna_error_cuando_repo_falla() {
        let mut mock = MockPedidoRepository::new();
        mock.expect_obtener()
            .with(eq(id_prueba()))
            .returning(|_| Err(AppError::NotFound("no existe".into())));

        let service = PedidoService::new(Arc::new(mock));
        let resultado = service.obtener(id_prueba()).await;

        assert!(matches!(resultado, Err(AppError::NotFound(_))));
    }
}
```

### assert_matches! para pattern matching en assertions

```rust
use std::assert_matches::assert_matches;

let resultado = service.crear(req_invalido).await;
assert_matches!(
    resultado,
    Err(AppError::Validation(msg)) if msg.contains("cliente_id")
);
```

### Configurar cargo-llvm-cov

```toml
# .cargo/config.toml
[alias]
cov = "llvm-cov --lcov --output-path lcov.info"
cov-html = "llvm-cov --html"
```

```bash
cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
# Verificar cobertura mínima del 80%
cargo llvm-cov --all-features --workspace --fail-under-lines 80
```

## Anti-patrones conocidos

### unwrap() en tests sin mensaje de contexto

```rust
// MAL — panic con mensaje generico "called unwrap on None"
let pedido = repo.obtener(id).await.unwrap();

// BIEN — mensaje que indica que estaba probando
let pedido = repo.obtener(id).await
    .expect("deberia encontrar el pedido insertado en el fixture");
```

### Tests que dependen de orden de ejecución

```rust
// MAL — modifica estado global, falla si otro test corre primero
static CONTADOR: AtomicU32 = AtomicU32::new(0);
#[test]
fn test_a() { CONTADOR.fetch_add(1, Ordering::SeqCst); }
#[test]
fn test_b() { assert_eq!(CONTADOR.load(Ordering::SeqCst), 1); } // fragil

// BIEN — cada test es autocontenido
#[test]
fn test_b() {
    let mut contador = 0u32;
    incrementar(&mut contador);
    assert_eq!(contador, 1);
}
```

### Doc tests para ejemplos no compilables

```rust
/// Calcula el descuento.
///
/// # Examples
/// ```no_run
/// // MAL — no_run significa que nunca se verifica que compile
/// let d = calcular_descuento(100.0, 10);
/// ```
///
/// ```
/// // BIEN — el ejemplo compila y se ejecuta en cargo test
/// use mi_crate::calcular_descuento;
/// assert_eq!(calcular_descuento(100.0, 10), 90.0);
/// ```
```

## Checklist de verificación

- [ ] Tests unitarios en módulo #[cfg(test)] en el mismo archivo
- [ ] Tests async usan #[tokio::test], no block_on manual
- [ ] rstest para fixtures compartidos y casos parametrizados
- [ ] proptest para invariantes de dominio con inputs generados
- [ ] mockall con #[cfg_attr(test, automock)], no en código de producción
- [ ] assert_eq! con mensaje descriptivo en assertions no triviales
- [ ] cargo-llvm-cov configurado con umbral mínimo del 80%

## Gotchas / Errores comunes no obvios

**`#[tokio::test]` sin `flavor = "multi_thread"` limita el paralelismo en tests async**: por defecto `#[tokio::test]` usa el runtime current-thread — si un test lanza múltiples tareas que esperan entre sí, puede producir un deadlock en el runtime single-thread que no ocurre en producción. Fix: usar `#[tokio::test(flavor = "multi_thread", worker_threads = 2)]` para tests que simulan concurrencia real.

**`mockall::automock` en un trait con métodos de retorno de tipo genérico requiere declarar el tipo concreto al configurar la expectativa**: `mock.expect_obtener().returning(|| vec![...])` puede fallar si el compilador no infiere el tipo de retorno genérico. Fix: especificar el tipo explícitamente en el `returning` con turbofish si es necesario, o definir un tipo concreto de retorno en el trait en lugar de genérico.

**Tests de integración en `tests/` no tienen acceso a funciones privadas o internas del crate**: si el test necesita inspeccionar estado interno que no es `pub`, el test de integración no puede hacerlo directamente. Causa: `tests/` es un crate separado que solo ve la API pública. Fix: mover el test a un módulo `#[cfg(test)]` inline en el módulo que necesita inspeccionar, o exponer una función de test pública con `#[doc(hidden)] pub fn __test_only_estado(&self)`.

**`proptest!` genera casos aleatorios que pueden no reproducirse sin la semilla**: cuando proptest encuentra un contraejemplo, lo imprime y lo guarda en `.proptest-regressions/`. Si el archivo no se commitea, el CI puede pasar sin el contraejemplo pero el bug persiste. Fix: committear el directorio `.proptest-regressions/` al repositorio para que el CI reproduzca los casos que fallaron en desarrollo.

---
*Skill creado con swl:crear-skill el 2026-03-31. Versión 1.0.0.*
