En Rust, la estrategia de pruebas se divide fundamentalmente en dos categorías: tests unitarios y tests de integración. La distinción no es solo conceptual, sino que dicta la estructura de archivos y la visibilidad de los elementos del código.

Los tests unitarios tienen como objetivo verificar funciones o métodos de forma aislada. El patrón estándar consiste en colocar un módulo llamado tests dentro del mismo archivo donde reside el código fuente, envuelto en el atributo #[cfg(test)]. Este atributo indica al compilador que el módulo solo debe compilarse cuando se ejecuten pruebas, evitando que el código de test se incluya en el binario final de producción. Al estar dentro del mismo módulo, el sub-módulo tests puede usar use super::*; para importar todos los elementos del padre, lo que le otorga acceso a funciones y estructuras privadas. Si intentas mover un test unitario que accede a miembros privados a una carpeta externa, el compilador fallará.

Los tests de integración, por otro lado, verifican que las distintas partes de una librería funcionen correctamente juntas, tratando a la librería como un cliente externo. Estos archivos se ubican en el directorio tests/ en la raíz del proyecto. Cada archivo en tests/ es tratado por Rust como un crate separado, lo que significa que solo pueden acceder a la API pública (pub) de tu biblioteca. Si necesitas compartir lógica de ayuda (como la configuración de una base de datos de prueba) entre varios archivos de integración, puedes crear un módulo tests/common/mod.rs. El compilador no tratará a este archivo como un test, permitiéndote reutilizar código de soporte.

Para ejecutar las pruebas, cargo test es el comando base. Puedes filtrar pruebas usando un string específico (ej. cargo test test_name) o listar todas con -- --list. Si necesitas ver los println! de tus tests, utiliza cargo test -- --nocapture. Para ejecutar solo la documentación con cargo test --doc, o para correr pruebas de forma secuencial (útil si los tests comparten recursos globales) usa -- --test-threads=1.

// src/lib.rs

pub struct Procesador;

impl Procesador {
    /// Método público que representa la API de la librería
    pub fn procesar(&self, entrada: &str) -> String {
        let validado = self.validar_entrada(entrada);
        if validado {
            format!("Procesado: {}", entrada.to_uppercase())
        } else {
            "Error: Entrada inválida".to_string()
        }
    }

    /// Método privado: solo accesible mediante tests unitarios
    fn validar_entrada(&self, entrada: &str) -> bool {
        !entrada.is_empty()
    }
}

impl Default for Procesador {
    fn default() -> Self {
        Self
    }
}

// --- SECCIÓN DE TESTS UNITARIOS ---

#[cfg(test)]
mod tests {
    // Importa todo desde el módulo padre (Procesador y sus métodos)
    use super::*;

    #[test]
    fn test_procesar_sucesso() {
        let p = Procesador::default();
        // Probamos la API pública
        assert_eq!(p.procesar("hola"), "Procesado: HOLA");
    }

    #[test]
    fn test_validar_entrada_privada() {
        let p = Procesador::default();
        // Gracias a 'use super::*', podemos testear 'validar_entrada' 
        // aunque sea un método privado.
        assert!(p.validar_entrada("cualquier cosa"));
        assert!(!p.validar_entrada(""));
    }
}

Análisis del código

  • Procesador: Es la estructura pública (pub) que define nuestra lógica.
  • procesar: Un método público que actúa como punto de entrada principal. Es lo que verían los tests de integración en el directorio tests/.
  • validar_entrada: Un método privado. Es imposible testearlo desde un test de integración, pero es perfectamente accesible dentro del módulo de tests unitarios.
  • #[cfg(test)]: Este atributo asegura que el bloque mod tests no se compile en la versión de producción de la librería, optimizando el tamaño del binario.
  • mod tests: Crea un sub-módulo para organizar las pruebas.
  • use super::*;: Esta es la instrucción clave. Permite que el módulo tests acceda al ámbito de su padre (src/lib.rs). Sin esto, tests no podría ver ni a Procesador ni al método validar_entrada.
  • test_procesar_sucesso: Un test unitario que verifica el flujo principal mediante la API pública.
  • test_validar_entrada_privada: Demuestra la capacidad de los tests unitarios para verificar la lógica interna de un componente sin necesidad de exponerla al usuario final de la librería.

111