Rusty Full Stack

El blog para los amantes de Rust, Ionic y Vuejs

Jaime Blandón
Jaime Blandón Desarrollador de software desde 2009, entusiasta de Rust, Vuejs y Ionic!. Fundador de este blog, espero que las publicaciones te sean de utilidad y si tienes comentarios para mejorar, son bienvenidos.

Mocks en Rust y Code Coverage

En los artículos anteriores, hemos aprendido sobre la importancia de crear pruebas automatizadas con Rust, por si aún no lo has leído, te invito a echarles un vistazo 😉

Pruebas Unitarias Con Rust.

Pruebas de Integración con Rust.

En esta ocasión vamos a finalizar esta mini-serie aprendiendo sobre los mocks y el code coverage con Rust.

¿Qué son los Mocks y Cómo Construirlos En Rust?

Los mocks nos ayudan a reemplazar estructuras o funciones reales, por otras a las cuales podemos definirles algún valor de retorno o le podemos definir qué es lo que esperamos en uno y otro escenario.

Si ya nuestras funciones o métodos tienen programado el comportamiento esperado, entonces ¿Por qué utilizar mocks?

Esta es una pregunta bastante válida y la respuesta puede variar de entorno de trabajo en entorno de trabajo, pero para mí, la respuesta más general es que la teoría menciona que las pruebas unitarias deberían ser rápidas, atómicas y obtener un resultado determinista.

Imagina que tu función o método tenga que hacer alguna operación en una base de datos, o una llamada a un API o incluso escribir algún archivo. En los tres casos anteriores tenemos el problema que tanto el estado de la Base de datos o del file system o incluso el API (que generalmente es una caja negra para los consumidores) son entornos cambiantes y que no están bajo nuestro control por lo que conectarnos a ellos directamente para probar no es una buena decisión.

Solamente imagina que para correr una prueba unitaria debas escribir un registro en una tabla de base de datos, luego leer la tabla y obtener un total de registros, si al escribir el registro llega otro por algún otro consumidor, entonces ya nuestro test fallaría.

Incluso si llegases a tener un sandbox de prueba, hay que recordar estar limpiando el sandbox cada vez que ejecutemos nuestras pruebas, por lo que podemos llegar a sobre complicar las pruebas más que el desarrollo mismo del software, lo cual no tiene nada de malo, pero en ocasiones utilizar mock nos puede brindar el mismo resultado esperado y haciéndolo de una forma bastante sencilla.

Otra razón de utilizar mocks, es que en ocasiones y dependiendo de la empresa en la que trabajemos, existen procesos o CI/CD automatizados que corren las pruebas antes de crear una nueva versión de nuestra aplicación, muchas veces estos procesos registren, por seguridad, la conexión a agentes externos como Bases de datos, o API's, por lo que únicamente utilizando mocks podremos verdaderamente comprobar que nuestro código ha sido probado.

Sé que a lo mejor lo anterior puede llegar a sonar complicado o sin sentido, pero no hay nada mejor para aprender que hacerlo con un ejemplo 🤩

¿Cómo Construir Un Mock Con Rust?

Existen varios crates para poder hacer mocks con Rust, en esta ocasión vamos a hacerlo con un crate llamado mockall, el cual es quizá uno de los más populares al momento.

Mockall, nos permitirá construir nuestros mocks y poder configurar lo que esperamos obtener como valores de respuesta. Como su nombre lo indica, podremos crear mocks de cualquier cosa como traits, estructuras, secuencias, funciones estáticas y mucho más 😎

Disclaimer si vienes desde Python.

Quisiera hacer en este momento un pequeño disclaimer para quienes venimos del mundo python. En python, existe un concepto llamado patch. Hacer patch (@mock.patch) consiste en que al ejecutar nuestros tests, le podemos decir a python que reemplace (hacer patch) un comportamiento determinado. Aunque patch es algo bastante conveniente, tiene el problema, que no es posible asignar un espacio o memoria específica por lo que los tests pueden llegar a ser "lentos" a comparación de otros lenguajes.

En el caso de Rust, por ser un lenguaje que optimiza memoria, necesita conocer o asignar de antemano el espacio requerido para una prueba por lo que no podremos realizar acciones tipo patch, sino, que deberemos pasar como parámetros las estructuras o traits con los métodos a los cuales les deberemos hacer patch, esto podría sonar complicado, pero en realidad, ayuda mucho que al momento de escribir nuestro código, seguir este tipo de modelo o diseño pues es cuando Rust hace su magia y hace que nuestros programas corran bastante rápido 😏

Fin del disclaimer si vienes de Python.

Creando Mocks En Rust Con Mockall.

Vamos a crear nuestro ejemplo el cual será una librería en Rust que simulará una conexión a una base de datos para obtener un pago en base a su id (identificador), pero también existirán un par más de funciones que simularán una petición a un API que necesita dos parámetros y calcula la suma de ambos y la otra función recibirá un número y se lo pasará a nuestro API simulado que deberá devolver si el número es válido o no.

Como hemos hablado anteriormente, tanto las conexiones a las base de datos como una conexión a un API no es algo que queramos hacer directamente en nuestras pruebas unitarias, sino, que deberemos hacer un mock para que simulen ese comportamiento.

Comencemos creando nuestra librería con Cargo 😊

En una terminal, en el directorio de tu preferencia ejecuta el siguiente comando:

cargo new --lib mocks_con_rust

cd mocks_con_rust

Recuerda que puedes ver el código completo en nuestro repositorio en github dando click acá.

Luego de haberlo creado deberíamos tener una estructura de archivos similar a la siguiente:

Lo primero que vamos a hacer es agregar a nuestras dependencias a mockall, modifiquemos nuestro archivo Cargo.toml

        
        
            // Cargo.toml

[package]
name = "mocks_con_rust"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
mockall = "0.11.3"
        
        
    

Mockall es un crate bastante amplio con muchísimas funcionalidades bastantes útiles que facilita la construcción de nuestros mocks y ejecutar nuestras pruebas unitarias. Cubrir todas las funcionalidades de mockall sería imposible en una sola publicación por lo que vamos a explorar las bases y de esta forma te invito a que si quieres especializarte en mockall, puedas darle un vistazo a su documentación la cual es bastante completa:

Documentación de Mockall

En nuestro ejemplo vamos a hacer nuestros mocks tomando de base a los Traits, por si no sabes que son los traits, o nunca habías escuchado de ellos, son el equivalente a las clases o métodos abstractos en otros lenguajes.

Hacer mocks a structs en Rust es muy parecidas que hacerlo con tratis. para crear mocks de estructuras puedes ver estos ejemplos:

Mocks a estructuras con mockall

Continuemos con nuestro ejemplo, lo primero recordar es que hemos hablado anteriormente en:

Pruebas Unitarias Con Rust.

que por convención, las pruebas unitarias deben escribirse en el módulo, entonces debemos recordar que el import a mockall y todos sus módulos deberemos agregarle el macro:

#([cfg(test)]

antes del import, nuestro archivo src/lib.rs debe verse por el momento así:

        
        
            // src/lib.rs

#[cfg(test)]
use mockall::{automock, predicate::*};
        
        
    

Como puedes ver en la línea anterior, estaremos importando mockall única y exclusivamente al ejecutar nuestras pruebas, no estará presente en el binario de ejecución.

También podemos ver que estamos importando un módulo llamado automock, este módulo, nos permite crear nuestros mocks de forma automática 😬

El siguiente módulo que estamos importando es el de predicates, este módulo no permite poder hacer algunas operaciones, como por ejemplo identificar si estamos invocando algún método con los parámetros esperados o condicionar que el resultado de nuestro mock sea diferente de acuerdo a los inputs que le pongamos.

Ahora vamos a crear dos "manejadores" uno para conectarnos a nuestra base de datos y la otra para conectarnos a un API. Para este ejemplo no nos conectaremos realmente a la base de datos o al API, únicamente vamos a simular que existen. En la práctica, tus manejadores sí se conectarán a los recursos, pero por sencillez del ejemplo no lo haremos esta vez (deja en tus comentarios si quieres que aprendamos a conectarnos a una base datos 😎), modifiquemos nuestro src/lib.rs para agregarle los manejadores, esta vez los haremos como traits.

        
        
            // src/lib.rs

#[cfg(test)]
use mockall::{automock, predicate::*};

pub trait DatabaseHandler {
    fn ejecutar_query(&self, query: String);
}

pub trait ApiHandler {
    // El endpoint "retornara" la suma de ambos valores
    fn resultado_endpoint_calculos(&self, a: i32, b: i32) -> i32;

    // El endpoint "retornara" el "resultado" de una validacion
    fn resultado_endpoint_validacion(&self, valor: i32) -> bool;
}


        
        
    

Como puedes ver tanto el Databasehandler como el APIHandler son Traits, es decir que los métodos que están dentro de ellos en realidad no hacen nada, es necesario definirles su comportamiento con impl, pero como lo que queremos es nada más hacer mocks, no es necesario que definamos el comportamiento.

Hasta este momento nuestros traits no se pueden convertir en mocks, para ello debemos hacer dos cosas. La primera es ponerles el automock para que se comporten como tales, pero lo segundo es que se conviertan en mocks únicamente si estamos ejecutando las pruebas unitarias, para otros casos no queremos que se conviertan en mocks, para ello debemos agregarle la configuración de esta forma en src/lib.rs

        
        
            // src/lib.rs

#[cfg(test)]
use mockall::{automock, predicate::*};

#[cfg_attr(test, automock)]
pub trait DatabaseHandler {
    fn ejecutar_query(&self, query: String);
}

#[cfg_attr(test, automock)]
pub trait ApiHandler {
    // El endpoint "retornara" la suma de ambos valores
    fn resultado_endpoint_calculos(&self, a: i32, b: i32) -> i32;

    // El endpoint "retornara" el "resultado" de una validacion
    fn resultado_endpoint_validacion(&self, valor: i32) -> bool;
}

        
        
    

Agregando:

#[cfg_attr(test, automock)]

antes de la definición de los traits, los estamos configurando para que cuando se ejecuten las pruebas unitarias, entonces mockall los convierta en mocks de forma automática 🥳.

Ahora recuerda el pequeño disclaimer sobre python, en el cual concluimos que a mejor forma de diseñar nuestro código, es el pasar como parámetros nuestros manejadores a los métodos o funciones que los utilizarán y de esta forma Rust optimizará la memoria.

Agreguemos entonces las funciones que también serán las que vamos a probar de nuestro módulo. En nuestro archivo src/lib.rs vamos a colocar el siguiente código:

        
        
            // src/lib.rs

#[cfg(test)]
use mockall::{automock, predicate::*};

#[cfg_attr(test, automock)]
pub trait DatabaseHandler {
    fn ejecutar_query(&self, query: String);
}

#[cfg_attr(test, automock)]
pub trait ApiHandler {
    // El endpoint "retornara" la suma de ambos valores
    fn resultado_endpoint_calculos(&self, a: i32, b: i32) -> i32;

    // El endpoint "retornara" el "resultado" de una validacion
    fn resultado_endpoint_validacion(&self, valor: i32) -> bool;
}

pub fn obtener_usuario_de_base_de_datos(db: Box<dyn DatabaseHandler>, id: i32) {
    let query = format!("SELECT * FROM usuarios WHERE id={}", id);
    db.ejecutar_query(query);
}

pub fn llamar_endpoint_calculos(http: Box<dyn ApiHandler>, a: i32, b:i32) -> i32 {
    // En la practica, probablemente este call sea asincrono (async/await)
    http.resultado_endpoint_calculos(a, b)
}

pub fn llamar_endpoint_validacion(http: Box<dyn ApiHandler>, valor: i32) -> bool {
    // En la practica, probablemente este call sea asincrono (async/await)
    http.resultado_endpoint_validacion(valor)
}

        
        
    

La función obtener_usuario_de_base_de_datos recibe como parámetros dos valores

el primero es un Box pointer dinámico, que es la forma que Rust tiene para asignar memoria a los traits que se envían como parámetro, porque a diferencia de los structs, los traits no se puede conocer a cabalidad su memoria final porque falta su implementación y el segundo parámetro es un id.

Como hemos visto estamos pasando el handler como parámetro y Rust hace el resto por nosotros.

Esta primera función no retorna nada pero si simulará que ejecuta un query.

Las otras funciones

llamar_endpoint_calculos

llamar_endpoint_validacion

son muy similares pero servirán para simular un call a un API, el llamar_endpoint_calculos simula que hace el request a un API que hace suma de valores y el llamar_endpoint_validación simula una validación que devolverá true o false dependiendo de los valores que se les envíen.

Ahora vamos a crear nuestra primera prueba unitaria con el mock de la base de datos.

Como la función obtener_usuario_de_base_de_datos no retorna nada, entonces vamos a asegurarnos que en caso se pase el id 22, la función que ejecuta un query de base de datos se ejecute una única vez.

Agreguemos nuestras pruebas unitarias al final de nuestro archivo src/lib.rs

        
        
            // src/lib.rs

#[cfg(test)]
use mockall::{automock, predicate::*};

#[cfg_attr(test, automock)]
pub trait DatabaseHandler {
    fn ejecutar_query(&self, query: String);
}

#[cfg_attr(test, automock)]
pub trait ApiHandler {
    // El endpoint "retornara" la suma de ambos valores
    fn resultado_endpoint_calculos(&self, a: i32, b: i32) -> i32;

    // El endpoint "retornara" el "resultado" de una validacion
    fn resultado_endpoint_validacion(&self, valor: i32) -> bool;
}

pub fn obtener_usuario_de_base_de_datos(db: Box<dyn DatabaseHandler>, id: i32) {
    let query = format!("SELECT * FROM usuarios WHERE id={}", id);
    db.ejecutar_query(query);
}

pub fn llamar_endpoint_calculos(http: Box<dyn ApiHandler>, a: i32, b:i32) -> i32 {
    // En la practica, probablemente este call sea asincrono (async/await)
    http.resultado_endpoint_calculos(a, b)
}

pub fn llamar_endpoint_validacion(http: Box<dyn ApiHandler>, valor: i32) -> bool {
    // En la practica, probablemente este call sea asincrono (async/await)
    http.resultado_endpoint_validacion(valor)
}

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

    #[test]
    fn probar_que_el_query_se_ejecuta_unicamente_una_vez() {
        let mut mock_db = Box::new(MockDatabaseHandler::new());

        mock_db.expect_ejecutar_query()
               .with(
                   eq("SELECT * FROM usuarios WHERE id=22".to_owned())
               )
               .once()
               .returning(|_query| ());

        obtener_usuario_de_base_de_datos(mock_db, 22);
    }
}

        
        
    

Si ahora ejecutamos nuestras pruebas unitarias con el siguiente comando:

cargo test

Vamos a ver que todos nuestros tests han pasado

pero esto debería de llevarnos a hacer la siguiente pregunta ¿si no hemos hecho la implementación de los traits y no tenemos ninguna base de datos o un API cómo es que nuestros tests han pasado?

La respuesta a lo anterior es porque en realidad nuestras pruebas unitarias fueron ejecutadas utilizando nuestros mocks. Podemos analizar e siguiente bloque de la prueba que acabamos de agregar:

        
        
            // prueba unitaria para para obtener un usuario de base de datos:

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

    #[test]
    fn probar_que_el_query_se_ejecuta_unicamente_una_vez() {
        let mut mock_db = Box::new(MockDatabaseHandler::new());

        mock_db.expect_ejecutar_query()
               .with(
                   eq("SELECT * FROM usuarios WHERE id=22".to_owned())
               )
               .once()
               .returning(|_query| ());

        obtener_usuario_de_base_de_datos(mock_db, 22);
    }
}

        
        
    

Lo primero a notar es esta línea:

        
        
            

let mut mock_db = Box::new(MockDatabaseHandler::new());
        
        
    

el módulo mockall crea nuestros mocks de forma automática (con el macro automock), colocándoles el mismo nombre que les hemos dado al trait o estructura pero la antecede con el prefijo "Mock"

Por ejemplo ya que nuestro trait se llama DatabaseHandler, entonces con el prefijo su versión mock se llamaría MockDatabaseHandler tal cual en la línea de código. Otra cosa que es importante a notar es que se ha declarado el mock como algo mutable (mut), esto se debe a que lo siguiente que debemos hacer es definir lo que esperamos comprobar.

Para configurar lo que esperamos de un método, podemos utilizar el prefijo "expect_" el cual se lo podemos anteponer el método que queremos configurar, por ejemplo el DatabaseHandler tiene un método llamado ejecutar_query, por lo que podemos configurar lo que esperamos con "expect" de esta forma, este código:

        
        
            

mock_db.expect_ejecutar_query()
               .with(
                   eq("SELECT * FROM usuarios WHERE id=22".to_owned())
               )
               .once()
               .returning(|_query| ());
        
        
    

lo que configura es que esperamos que el método "ejecutar_query" de nuestro mock con ( .with ) el predicado eq o igual al query "SELECT * FROM usuarios WHERE id=22" se ejecute una única vez (.once() ) y que no retorne nada ( .returning(|_query| ()) )

Luego al correr esta línea en nuestra prueba unitaria:

        
        
            
obtener_usuario_de_base_de_datos(mock_db, 22);
        
        
    

se ejecuta y coloca justamente el query que espera el método debido a que le estamos pasando el 22 y estamos esperando "SELECT * FROM usuarios WHERE id=22"

Si cambias por un momento la línea anterior por:

        
        
            
obtener_usuario_de_base_de_datos(mock_db, 11);
        
        
    

Ahora colocaría el query "SELECT * FROM usuarios WHERE id=11" el cual no es el esperado, entonces al correr nuestras pruebas, estas fallan.

cargo test

(Vuelve a colocar el valor 22 a nuestro test anterior para que pueda pasar sin problema)

Hemos creado nuestra prueba unitaria con mocks en Rust!! 🥳

Ahora antes de continuar podríamos preguntarnos si todo el comportamiento de nuestro módulo es el esperado o si estamos probando todas nuestras funciones, para ello vamos a aprender lo siguiente:

¿Cómo Obtener el Code Coverage Con Rust?

El code coverage no es más que el porcentaje de líneas de código que estamos probando, también nos indica aquellas líneas o funciones que no están siendo probadas.

Aunque el 100% del code coverage no nos asegure que exista algún bug o error no identificado, sí es un buen parámetro para que al menos sepamos que estamos haciendo las pruebas unitarias básicas que necesitamos.

Existen varias formas de obtener el code coverage con Rust. Una forma que me gusta mucho es utilizando la utileria de cargo llamada tarpaulin

Puede instalar tarpaulin ejecutando el siguiente comando en una terminal:

cargo install cargo-tarpaulin

Tarpaulin, tiene muchas opciones, puedes ver la documentación completa en:

página de github de tarpaulin

Después de instalar tarpaulin, ahora exploremos el code coverage de nuestro programa en Rust e identificar lo que estamos y no estamos cubriendo 🤓

Vamos a hacerlo de una forma bastante interactiva y fácil de utilizar como una página html. En una terminal ejecuta el siguiente comando (recuerda que todas las pruebas unitarias deben pasar para obtener el code coverage):

cargo tarpaulin -v --out Html

El comando anterior:

  • Ejecutará las pruebas unitarias
  • Calculará el code coverage
  • Creará un informe en html.

Verás que en tu terminal estarán pasando muchas cosas, lo cual es norma, al final de la ejecución verá algo similar a esto:

Ese es el resumen del code coverage en la terminal.

Lo primero que podemos observar son las líneas que no están siendo cubiertas por nuestras pruebas, así como al archivo al que pertenecen.

src/lib.rs: 10, 23-25, 28-30

Luego veremos el total de líneas que sin son cubiertas por nuestras pruebas unitarias:

src/lib.rs: 4/11

Finalmente veremos el porcentaje o code coverage de nuestras pruebas unitarias, en este caso, nuestras pruebas cubren únicamente el 36.36% de nuestro código

36.36% coverage

Pero además de este resumen en consola, también el comando anterior ha generado un informe en html que podemos abrir en nuestro browser, el informe por defecto lo guarda en la carpeta root de nuestro proyecto:

puedes ver que ha creado un archivo html llamado tarpaulin-report.html, que si lo abres en tu navegador verás un reporte bastante interactivo con el code coverage de nuestro programa en Rust.

Como puedes ver, en la imagen anterior nos muestra el archivo y su code coverage, también nos muestra que está con un fondo rojizo lo que quiere decir que el code coverage es bastante bajo, si damos click a ese grid también vamos a ver que nos muestra las líneas de código cubiertas por nuestras pruebas (fondo verde) y aquellas que no están siendo cubiertas (fondo rojo)

Ahora ya hemos aprendido a obtener un resumen bastante bonito e interactivo sobre nuestro code coverage en Rust 😄

Como hemos visto que nuestro code coverage es bajito, vamos a aumentarlo un poco más así también aprendemos como retornar valores con nuestros mocks💪

Retornando Valores Con Mocks En Rust.

Continuando con nuestro ejemplo anterior vamos a cubrir el método llamar_endpoint_calculos, para ello agreguemos el siguiente código en src/lib.rs para agregar un caso de prueba para ese método, lo que vamos a probar es que invocamos al api y esperamos que nuestro mock nos devuelva la suma de los parámetros:

        
        
            // src/lib.rs

#[cfg(test)]
use mockall::{automock, predicate::*};

#[cfg_attr(test, automock)]
pub trait DatabaseHandler {
    fn ejecutar_query(&self, query: String);
}

#[cfg_attr(test, automock)]
pub trait ApiHandler {
    // El endpoint "retornara" la suma de ambos valores
    fn resultado_endpoint_calculos(&self, a: i32, b: i32) -> i32;

    // El endpoint "retornara" el "resultado" de una validacion
    fn resultado_endpoint_validacion(&self, valor: i32) -> bool;
}

pub fn obtener_usuario_de_base_de_datos(db: Box<dyn DatabaseHandler>, id: i32) {
    let query = format!("SELECT * FROM usuarios WHERE id={}", id);
    db.ejecutar_query(query);
}

pub fn llamar_endpoint_calculos(http: Box<dyn ApiHandler>, a: i32, b:i32) -> i32 {
    // En la practica, probablemente este call sea asincrono (async/await)
    http.resultado_endpoint_calculos(a, b)
}

pub fn llamar_endpoint_validacion(http: Box<dyn ApiHandler>, valor: i32) -> bool {
    // En la practica, probablemente este call sea asincrono (async/await)
    http.resultado_endpoint_validacion(valor)
}

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

    #[test]
    fn probar_que_el_query_se_ejecuta_unicamente_una_vez() {
        let mut mock_db = Box::new(MockDatabaseHandler::new());

        mock_db.expect_ejecutar_query()
               .with(
                   eq("SELECT * FROM usuarios WHERE id=22".to_owned())
               )
               .once()
               .returning(|_query| ());

        obtener_usuario_de_base_de_datos(mock_db, 22);
    }

    #[test]
    fn probar_que_obtenga_el_calculo_correcto_del_api_handler() {
        let mut mock_api_handler = Box::new(MockApiHandler::new());

        let a = 1;
        let b = 1;

        mock_api_handler.expect_resultado_endpoint_calculos()
                        .once()
                        .returning(|a, b| a + b);

        let observado = llamar_endpoint_calculos(mock_api_handler, a, b);
        let esperado = 2;
        assert_eq!(observado, esperado);
    }
}

        
        
    

La siguiente prueba unitaria:

        
        
            

#[test]
    fn probar_que_obtenga_el_calculo_correcto_del_api_handler() {
        let mut mock_api_handler = Box::new(MockApiHandler::new());

        let a = 1;
        let b = 1;

        mock_api_handler.expect_resultado_endpoint_calculos()
                        .once()
                        .returning(|a, b| a + b);

        let observado = llamar_endpoint_calculos(mock_api_handler, a, b);
        let esperado = 2;
        assert_eq!(observado, esperado);
    }
        
        
    

Configura nuestro mock de la siguiente forma:

el expect_resultado_endpoint_calculos configura que cuando el API handler ejecute el método resultado_endpoint_calculos, entonces se espera que el método se ejecute una única vez ( .once() ) y que lo que el mock debe retornar como valor esperado es la sumatoria del parámetro a más el parámetro b ( .returning(|a, b| a + b) ).

y luego hace el assert_eq! del valor observado, junto con el esperado que debería ser 2 porque a y b son 1

Recuerda que la prueba anterior no se está conectando a un verdadero API, sino, que hace un mock que simula la conexión, ahora veamos si nuestro code coverage en verdad aumenta utilizando nuestro mock, anteriormente nuestro code coverage era 36.36%, ejecutemos el siguiente comando para actualizar nuestro informe de code coverage en html:

cargo tarpaulin -v --out Html

Si al ejecutarlo volvemos a revisar nuestro coverage veremos que ahora ha aumentado!

Por lo que podemos concluir que utilizar mocks nos está ayudando a probar nuestro código sin necesidad de conectarnos verdaderamente a los recursos externos como un API 🫶

Con el ejemplo anterior puedes ponerte el reto de hacer el coverage de la función que hace falta llamar_endpoint_validacion, te propongo que intentes hacer dos casos de prueba, uno en el cual si recibe un 2 entonces configurar el mock del api para retornar un true y si recibe un 4 lo configures para retornar un false, abajo puedes encontrar la solución, pero te invito a intentar resolverlo por tu propia cuenta 😉

La solución al ejercicio anterior es:

        
        
            // src/lib.rs

#[cfg(test)]
use mockall::{automock, predicate::*};

#[cfg_attr(test, automock)]
pub trait DatabaseHandler {
    fn ejecutar_query(&self, query: String);
}

#[cfg_attr(test, automock)]
pub trait ApiHandler {
    // El endpoint "retornara" la suma de ambos valores
    fn resultado_endpoint_calculos(&self, a: i32, b: i32) -> i32;

    // El endpoint "retornara" el "resultado" de una validacion
    fn resultado_endpoint_validacion(&self, valor: i32) -> bool;
}

pub fn obtener_usuario_de_base_de_datos(db: Box<dyn DatabaseHandler>, id: i32) {
    let query = format!("SELECT * FROM usuarios WHERE id={}", id);
    db.ejecutar_query(query);
}

pub fn llamar_endpoint_calculos(http: Box<dyn ApiHandler>, a: i32, b:i32) -> i32 {
    // En la practica, probablemente este call sea asincrono (async/await)
    http.resultado_endpoint_calculos(a, b)
}

pub fn llamar_endpoint_validacion(http: Box<dyn ApiHandler>, valor: i32) -> bool {
    // En la practica, probablemente este call sea asincrono (async/await)
    http.resultado_endpoint_validacion(valor)
}

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

    #[test]
    fn probar_que_el_query_se_ejecuta_unicamente_una_vez() {
        let mut mock_db = Box::new(MockDatabaseHandler::new());

        mock_db.expect_ejecutar_query()
               .with(
                   eq("SELECT * FROM usuarios WHERE id=22".to_owned())
               )
               .once()
               .returning(|_query| ());

        obtener_usuario_de_base_de_datos(mock_db, 22);
    }

    #[test]
    fn probar_que_obtenga_el_calculo_correcto_del_api_handler() {
        let mut mock_api_handler = Box::new(MockApiHandler::new());

        let a = 1;
        let b = 1;

        mock_api_handler.expect_resultado_endpoint_calculos()
                        .once()
                        .returning(|a, b| a + b);

        let observado = llamar_endpoint_calculos(mock_api_handler, a, b);
        let esperado = 2;
        assert_eq!(observado, esperado);
    }

    #[test]
    fn probar_que_el_endpoint_de_validacion_devuelve_un_true_si_se_envia_un_2() {
        let mut mock_api_handler = Box::new(MockApiHandler::new());

        let valor = 2;

        mock_api_handler.expect_resultado_endpoint_validacion()
                        .with(eq(2))
                        .return_once(|_valor| true);

        let observado = llamar_endpoint_validacion(mock_api_handler, valor);
        assert!(observado);
    }

    #[test]
    fn probar_que_el_endpoint_de_validacion_devuelve_un_true_si_se_envia_un_4() {
        let mut mock_api_handler = Box::new(MockApiHandler::new());

        let valor = 4;

        mock_api_handler.expect_resultado_endpoint_validacion()
                        .with(eq(4))
                        .return_once(|_valor| false);

        let observado = llamar_endpoint_validacion(mock_api_handler, valor);
        assert!(!observado);
    }
}

        
        
    

Como puedes observar en el código anterior, si quieres retornar una valor una única vez puedes utilizar .return_once

Ahora nuestro si volvemos a generar nuestro informe de code coverage, veremos que hemos alcanzado el 100%

No es que alcanzar el 100% deba ser tu finalidad, recuerda que debes priorizar las necesidades de negocio, pero establecerte un umbral de coverage (entre 80%-90%) y crear pruebas unitarias que satisfagan los casos que quieres probar sea suficiente.

Recuerda que puedes ver el código completo en nuestro repositorio en github dando click acá.

En este artículo hemos aprendido a crear nuestros mocks con Rust y también a crear nuestro reporte de code coverage 😊

Si este artículo te ha sido de utilidad, compártelo en tus redes sociales y con tus amigos, no te olvides de seguirme en twitter

Deja en la caja de comentarios aquellos temas sobre Rust que te gustaría aprendamos juntos 😎

println!("Hasta la próxima!");


 Utilizamos cookies propias y de terceros para mejorar tu experiencia, mostrar publicidad y análisis de navegación, puedes encontrar el detalle en nuestra Política de Cookies