Testing

Organización de pruebas

Las soluciones .NET utilizan proyectos separados para alojar el código de prueba, independientemente del framework de pruebas utilizado (xUnit, NUnit, MSTest, etc.) y el tipo de pruebas (unitarias o de integración) que se estén escribiendo. Por lo tanto, el código de los test vive en un espacio separado al código de la aplicación o biblioteca que se está probando. En Rust, es mucho más convencional que las pruebas unitarias se encuentren en un submódulo de prueba separado (convencionalmente) llamado tests, pero que se coloca en el mismo archivo fuente que el código del módulo de aplicación o biblioteca que es objeto de las pruebas. Esto tiene dos beneficios:

  • El código/módulo y sus pruebas unitarias viven juntos.

  • No hay necesidad de un truco como [InternalsVisibleTo] que existe en .NET porque las pruebas tienen acceso a los elementos internos al ser un submódulo.

El submódulo de prueba está anotado con el atributo #[cfg(test)], lo que tiene el efecto de que todo el módulo se compila (condicionalmente) y se ejecuta solo cuando se emite el comando cargo test.

Dentro de los submódulos de prueba, las funciones de prueba están anotadas con el atributo #[test].

Las pruebas de integración suelen estar en un directorio llamado tests que se encuentra adyacente al directorio src con las pruebas unitarias y el código fuente. cargo test compila cada archivo en ese directorio como un crate separado y ejecuta todos los métodos anotados con el atributo #[test]. Dado que se entiende que las pruebas de integración están en el directorio tests, no es necesario marcar los módulos allí con el atributo #[cfg(test)].

Mirar también:

Ejecución de pruebas

Tan simple como puede ser, el equivalente de dotnet test en Rust es cargo test.

El comportamiento predeterminado de cargo test es ejecutar todas las pruebas en paralelo, pero esto se puede configurar para que se ejecuten de manera consecutiva utilizando solo un hilo:

cargo test -- --test-threads=1

Para obtener más información, consulta "Ejecutando tests en paralelo o consecutivamente".

ejecutando-tests-en-paralelo-o-consecutivamente

Output en las Pruebas

Para pruebas de integración o de extremo a extremo muy complejas, a veces los desarrolladores de .NET registran lo que está sucediendo durante una prueba. La forma en que hacen esto varía con cada framework de pruebas. Por ejemplo, en NUnit, esto es tan simple como usar Console.WriteLine, pero en XUnit, se utiliza ITestOutputHelper. En Rust, es similar a NUnit; es decir, simplemente se escribe en la salida estándar usando println!. La salida capturada durante la ejecución de las pruebas no se muestra por defecto a menos que cargo test se ejecute con la opción --show-output:

cargo test --show-output

Para obtener más información, consulta "Mostrando el Output de las funciones".

mostrando-el-output-de-las-funciones

Aserciones

Los usuarios de .NET tienen múltiples formas de hacer aserciones, dependiendo del framework de trabajo que estén utilizando. Por ejemplo, una aserción en xUnit.net podría lucir así:

[Fact]
public void Something_Is_The_Right_Length()
{
    var value = "something";
    Assert.Equal(9, value.Length);
}

Rust no requiere un framework o crate separado. La biblioteca estándar viene con macros integradas que son lo suficientemente buenas para la mayoría de las afirmaciones en las pruebas:

A continuación se muestra un ejemplo de assert_eq en acción:

#[test]
fn something_is_the_right_length() {
    let value = "something";
    assert_eq!(9, value.len());
}

La biblioteca estándar no ofrece nada en la dirección de pruebas basadas en datos, como [Theory] en xUnit.net.

Mocking

Cuando se escriben pruebas para una aplicación o biblioteca .NET, existen varios framework, como Moq y NSubstitute, para simular las dependencias de los tipos. También hay crates similares para Rust, como mockall, que pueden ayudar con la simulación. Sin embargo, también es posible usar compilación condicional haciendo uso del atributo cfg como un medio simple para la simulación sin necesidad de depender de crates o frameworks externos. El atributo cfg incluye condicionalmente el código que anota en función de un símbolo de configuración, como test para pruebas. Esto no es muy diferente de usar DEBUG para compilar condicionalmente código específicamente para compilaciones de depuración. Una desventaja de este enfoque es que solo se puede tener una implementación para todas las pruebas del módulo.

Cuando se especifica, el atributo #[cfg(test)] le indica a Rust que compile y ejecute el código solo cuando se ejecute el comando cargo test, que ejecuta el compilador con rustc --test. Lo contrario es cierto para el atributo #[cfg(not(test))]; incluye el código anotado solo cuando se realiza la prueba con cargo test.

El ejemplo a continuación muestra la simulación de una función independiente var_os de la biblioteca estándar que lee y devuelve el valor de una variable de entorno. Importa condicionalmente una versión simulada de la función var_os utilizada por get_env. Cuando se compila con cargo build o se ejecuta con cargo run, el binario compilado hará uso de std::env::var_os, pero cargo test en su lugar importará tests::var_os_mock como var_os, lo que hará que get_env utilice la versión simulada durante las pruebas:

// Derechos de autor (c) Microsoft Corporation. Todos los derechos reservados.
// Licenciado bajo la licencia MIT.

/// Función de utilidad para leer una variable de entorno y devolver su valor 
/// si está definida. Falla/pániquea si el valor no es Unicode válido.
pub fn get_env(key: &str) -> Option<String> {
    #[cfg(not(test))]                 // para compilaciones regulares...
    use std::env::var_os;             // ...importar desde la biblioteca estándar
    #[cfg(test)]                      // para compilaciones de prueba...
    use tests::var_os_mock as var_os; // ...importar la simulación desde el submódulo de prueba

    let val = var_os(key);
    val.map(|s| s.to_str()     // obtiene slice de string
                 .unwrap()     // lanza un pánico si no es Unicode válido
                 .to_owned())  // convierte a "String"
}

#[cfg(test)]
mod tests {
    use std::ffi::*;
    use super::*;

    pub(crate) fn var_os_mock(key: &str) -> Option<OsString> {
        match key {
            "FOO" => Some("BAR".into()),
            _ => None
        }
    }

    #[test]
    fn get_env_when_var_undefined_returns_none() {
        assert_eq!(None, get_env("???"));
    }

    #[test]
    fn get_env_when_var_defined_returns_some_value() {
        assert_eq!(Some("BAR".to_owned()), get_env("FOO"));
    }
}

Cobertura de código

Existe herramientas sofisticadas para .NET en cuanto a análisis de cobertura de código de pruebas. En Visual Studio, las herramientas están integradas de forma nativa. En Visual Studio Code, existen complementos. Los desarrolladores de .NET podrían estar familiarizados con coverlet también.

Rust proporciona implementaciones integradas de cobertura de código para recopilar la cobertura de código de las pruebas.

También hay complementos disponibles para Rust que ayudan con el análisis de cobertura de código. No está integrado de manera perfecta, pero con algunos pasos manuales, los desarrolladores pueden analizar su código de manera visual.

La combinación del complemento Coverage Gutters para Visual Studio Code y Tarpaulin permite el análisis visual de la cobertura de código en Visual Studio Code. Coverage Gutters requiere un archivo LCOV. Se pueden usar otras herramientas además de Tarpaulin para generar ese archivo.

Una vez configurado, ejecuta el siguiente comando:

cargo tarpaulin --ignore-tests --out Lcov

Esto genera un archivo de cobertura de código LCOV. Una vez habilitado Coverage Gutters: Watch, será recogido por el complemento Coverage Gutters, que mostrará indicadores visuales en línea sobre la cobertura de líneas en el editor de código fuente.

Nota: La ubicación del archivo LCOV es esencial. Si hay un espacio de trabajo (ver Estructura del Proyecto) con múltiples paquetes y se genera un archivo LCOV en la raíz usando --workspace, ese es el archivo que se está utilizando, incluso si hay un archivo presente directamente en la raíz del paquete. Es más rápido aislar el paquete específico bajo prueba en lugar de generar el archivo LCOV en la raíz.