Rusty Full Stack
El blog para los amantes de Rust, Ionic y Vuejs
El blog para los amantes de Rust, Ionic y Vuejs
Este es el segundo episodio de nuestra mini serie sobre automatizar pruebas con Rust 😎
nuestro primer post trató sobre cómo construir pruebas unitarias, algunas convenciones y opciones disponibles para correr nuestras pruebas unitarias. Por si aún no lo has leído, puedes echarle un vistazo en el siguiente enlace:
Pruebas Unitarias con Rust - Módulos de Prueba
En este artículo hablaremos sobre las pruebas de integración con Rust!
Cuando empezaba a estudiar sobre pruebas unitarias y luego descubrí que también existían las pruebas de integración, me fue un poco complicado tratar de entender dónde comenzaba una y terminaba la otra 😵💫
Sin embargo, con un poco de práctica, se empieza a distinguirlas. Usualmente, los programas que escribimos están compuesto por uno ó más módulos, como comentábamos en el post anterior sobre pruebas unitarias, estas son bloques de código que validan que las funciones del módulo se comportan como lo esperamos.
También es cierto, que al final del día, nuestro programa es el producto final de la lógica de nuestros módulos funcionando entre sí, las pruebas de integración se encargan precisamente de verificar que la lógica al utilizar nuestros módulos de forma conjunta sea la correcta al igual que el intercambio de datos entre ellos.
A lo mejor la definición anterior siga siendo un poco confusa, pero vamos a verlo más claro con un par de ejemplos 😉
Algunos puntos que tenemos que tener en cuenta al momento de crear las pruebas de integración son las siguientes:
Manos a la obra!
Para empezar nuestro ejemplo, hace algunas publicaciones atrás en el artículo llamado:
Organizar Código en Rust - Diferentes Archivos.
Creamos un ejemplo que simulaba una caja registradora de un super mercado, en el ejemplo creamos diferentes módulos y distintos archivos. En el ejemplo de ese artículo, los módulos no contenían ninguna prueba unitaria o de integración.
Para comprender el ejemplo de pruebas de integración, no es necesario que conozcas el detalle del código o hayas leído el artículo anterior (pero te recomiendo leerlo 😋) ya que únicamente nos vamos a enfocar en un par de archivos bastante sencillos.
Un resumen de lo que realiza nuestra "caja registradora" es que va agregando o quitando items en un vector global y luego permite simular un pago con tres tipos de métodos: en efectivo, con tarjeta o por transferencia, por supuesto, que únicamente simula el pago no se conecta con ninguna plataforma de transferencia de dinero.
Para hacer nuestro ejemplo, hemos creado un repositorio con el código completo de nuestra caja registradora a la cual le hemos agregado pruebas unitarias (recuerda que puedes ver el artículo anterior sobre pruebas unitarias con Rust ☺️)
Empieza descargando o clonando el código completo, puedes encontrar el repo en el siguiente enlace, IMPORTANTE, debes utilizar la rama master:
caja-supermercado-multiarchivos-integration-tests
(El ejemplo completo puedes encontrarlo en la rama version-con-pruebas-de-integracion)
Cuando descargues o clones el repositorio, al examinar la estructura del proyecto de encontrarás con los siguientes módulos dentro de la carpeta src/
Como puedes ver hay 4 módulos.
Para nuestro ejemplo nos vamos a enfocar en el módulo operaciones, dentro del cual tenemos dos archivos compras.rs y pagos.rs
Cada archivo tiene sus propios unit tests como está especificado en las convenciones de las pruebas unitarias con Rust.
compras.rs:
Este archivo contiene todas las operaciones para agregar artículos, quitar artículos, calcular la cuenta y pagar, sus pruebas unitarias son:
// Pruebas unitarias de src/operaciones/compras.rs (a partir de la linea 52)
#[cfg(test)]
mod tests {
use crate::modelo_compras::Item;
use super::*;
#[test]
fn agregando_un_item_a_la_cuenta() {
let mut items_compra: Vec<Item> = Vec::new();
let item: Item = Item{
nombre: String::from("item de prueba"),
precio_unitario: 1.25,
cantidad: 1.0
};
agregar_item(&mut items_compra, item);
assert_eq!(items_compra[0].nombre, String::from("item de prueba"));
assert_eq!(items_compra[0].precio_unitario, 1.25);
assert_eq!(items_compra[0].cantidad, 1.0);
}
#[test]
fn quitando_un_item_a_la_cuenta() {
let mut items_compra: Vec<Item> = Vec::new();
let item1: Item = Item{
nombre: String::from("item de prueba1"),
precio_unitario: 1.25,
cantidad: 1.0
};
let item2: Item = Item{
nombre: String::from("item de prueba2"),
precio_unitario: 1.25,
cantidad: 1.0
};
items_compra.push(item1);
items_compra.push(item2);
assert_eq!(items_compra.len(), 2);
// Quitando item de prueba1
quitar_item(&mut items_compra, 0);
assert_eq!(items_compra.len(), 1);
assert_eq!(items_compra[0].nombre, String::from("item de prueba2"));
assert_eq!(items_compra[0].precio_unitario, 1.25);
assert_eq!(items_compra[0].cantidad, 1.0);
}
}
pagos.rs:
Este archivo contiene todas las operaciones para simular los pagos ya sea en efectivo, con tarjeta o por transferencia bancaria, sus pruebas unitarias son:
// Pruebas unitarias de src/operaciones/pagos.rs (a partir de la linea 68)
#[cfg(test)]
mod tests {
use crate::models::modelo_pagos::ResultadoPago;
use super::*;
#[test]
fn prueba_pago_en_efectivo() {
let observado = pago_en_efectivo(100.00, 110.00);
let esperado = ResultadoPago {
metodo_pago: String::from("En Efectivo"),
fue_exitoso: true,
cambio: 10.00
};
assert_eq!(observado.metodo_pago, esperado.metodo_pago);
assert!(observado.fue_exitoso);
assert_eq!(observado.cambio, esperado.cambio);
}
#[test]
fn prueba_pago_con_tarjeta() {
let observado = pago_con_tarjeta(100.00, r#"12345678-9"#);
let esperado = ResultadoPago {
metodo_pago: String::from("Tarjeta"),
fue_exitoso: true,
cambio: 0.00
};
assert_eq!(observado.metodo_pago, esperado.metodo_pago);
assert!(observado.fue_exitoso);
assert_eq!(observado.cambio, esperado.cambio);
}
#[test]
fn prueba_pago_por_transferencia_bancaria() {
let observado = pago_por_transferencia_bancaria(100.00);
let esperado = ResultadoPago {
metodo_pago: String::from("Transferencia Bancaria"),
fue_exitoso: true,
cambio: 0.00
};
assert_eq!(observado.metodo_pago, esperado.metodo_pago);
assert!(observado.fue_exitoso);
assert_eq!(observado.cambio, esperado.cambio);
}
}
Las pruebas unitarias anteriores están enfocadas a probar los métodos dentro de cada uno de los módulos, pero no están probando que la relación entre ellas funcione, es decir, NO están probando que por ejemplo un cliente llegue a la tienda, agregue unos items y luego pague con tarjeta de crédito.
Ese tipo de pruebas donde involucran más de un módulo son perfectas para hacer pruebas de integración.
Una buena práctica es crear alguna tabla con las pruebas de integración que queramos realizar, por ejemplo podríamos crear algo como esto:
Disclaimer: se pueden/deben incluir tantas pruebas de integración como los casos significativos para las reglas de negocio sean necesarios.
Al leer los casos de prueba anteriores, podemos notar que deberá haber una interacción entre el módulo operaciones::compras y operaciones::pagos
Empecemos editando el código de nuestro programa. Lo primero que debemos hacer es crear la carpeta tests como lo habíamos comentado que era la convención para las pruebas de integración, debemos crearla al mismo nivel de src, como se muestra en la imagen:
dentro de la carpeta tests vamos a colocar todos los archivos que tengan relación con nuestras pruebas de integración, nosotros, vamos a crear únicamente un archivo por sencillez del ejemplo y lo llamaremos tests/integration_tests.rs
Ahora hay algo importante a tomar en cuenta, recuerda que el crate se encuentra dentro de la carpeta src/ por lo que no será posible usar desde la carpeta tests los módulos de src si no convertimos nuestro crate en una librería.
Para ello vamos a crear un archivo src/lib.rs y vamos a darle a conocer nuestros módulos, otra alternativa es convertir cada módulo en una librería lo cual se está volviendo muy popular en estos tiempos (si quieres aprender más sobre esta forma de dividir los archivos escríbelo en la caja de comentarios 🙃)
Creamos nuestro archivo src/lib.rs deberá le colocamos este código, con el cual exponemos de forma pública nuestros módulos, recuerda solamente poner de forma pública los módulos que creas sean necesariamente públicos, en este caso colocamos todos solo por facilidad:
// src/lib.rs
pub mod menu;
pub mod models;
pub mod operaciones;
pub mod utileria;
Ahora ya podemos usar los módulos de nuestro crate, que se me había olvidado comentarte se llama caja_supermercado_multiarchivos (ver el archivo Cargo.toml) dentro de nuestro folder tests. También otra cosa que es importante comentar es que también el folder tests es considerado un crate dentro de Rust.
Ahora agreguemos dentro de nuestro archivo tests/integration_tests.rs agregamos los módulos que vamos a utilizar, para este caso serían los siguientes:
// tests/integration_tests.rs
use caja_supermercado_multiarchivos::models::modelo_compras;
use caja_supermercado_multiarchivos::models::modelo_pagos;
use caja_supermercado_multiarchivos::operaciones::compras;
use caja_supermercado_multiarchivos::operaciones::pagos;
Ahora ya podemos crear nuestras pruebas de integración, empecemos por la que dice:
Descripción: "Un cliente agrega 2 items y paga con tarjeta"
Caso de Prueba: "Agregar 2 items con el módulo de compras, luego calcular el total a pagar y utilizar el método de tarjeta"
Resultado Esperado: "Resultado Pago, el tipo debe ser "Tarjeta", fue_exitoso debe ser true y cambio debe ser 0.0"
Nuestro caso de integración lo colocamos dentro de tests/integration_tests.rs, le llamaremos cliente_paga_con_tarjeta_de_credito
// tests/integration_test.rs
use caja_supermercado_multiarchivos::models::modelo_compras;
use caja_supermercado_multiarchivos::models::modelo_pagos;
use caja_supermercado_multiarchivos::operaciones::compras;
use caja_supermercado_multiarchivos::operaciones::pagos;
// Es importante tomar en cuenta que
// como ahora estamos en otro folder que no es un modulo
// entonces no es necesario utilizar el mod tests o el
// #[cfg(test)]
#[test] // basta con agregar el macro tests
fn cliente_paga_con_tarjeta_de_credito() {
// Primero vamos a crear nuestro array de items
let mut items_compra: Vec<modelo_compras::Item> = Vec::new();
// Ahora vamos a agregar los 2 items
let item1: modelo_compras::Item = modelo_compras::Item {
nombre: String::from("item 1"),
precio_unitario: 50.00,
cantidad: 1.00
};
let item2: modelo_compras::Item = modelo_compras::Item {
nombre: String::from("item 2"),
precio_unitario: 150.00,
cantidad: 2.00
};
// Agregando los items
compras::agregar_item(&mut items_compra, item1);
compras::agregar_item(&mut items_compra, item2);
// Aca hacemos las primeras verificacione por ejemplo
// verificamos que en efecto haya unicamente dos items
assert_eq!(items_compra.len(), 2);
// si queremos podemos probar que el total es 350.00
assert_eq!(compras::total_compra(&items_compra), 350.00);
// Ahora probamos en pago como dice el caso de prueba
let resultado_pago = pagos::pagar(
modelo_pagos::MetodoDePago::Tarjeta, // metodo de pago
compras::total_compra(&items_compra), // total a pagar
350.00, // recibido del cliente
"12345" // numero de tarjeta
);
let resultado_esperado = modelo_pagos::ResultadoPago {
metodo_pago: String::from("Tarjeta"),
fue_exitoso: true,
cambio: 0.0
};
// Ahora verificamos que los resultados del pago sean los esperados
assert_eq!(resultado_pago.metodo_pago, resultado_esperado.metodo_pago);
assert_eq!(resultado_pago.fue_exitoso, resultado_esperado.fue_exitoso);
assert_eq!(resultado_pago.cambio, resultado_esperado.cambio);
}
Algunas cosas importantes a mencionar son:
También ahora si ejecutamos nuestros tests vamos a ver el siguiente output en nuestra consola, para probar tanto las pruebas unitarias como las de integración, siempre utilizaremos cargo:
cargo test
El output de nuestros tests es:
Podemos ver que los primeros en ejecutarse son las pruebas unitarias (running 5 tests) y luego se muestran las pruebas unitarias (running 1 test)
Para continuar con nuestras pruebas de integración, vamos a encargarnos de nuestra segunda prueba de integración a esta prueba de integración le vamos a llamar cliente_paga_en_efectivo_y_recibira_cambio.
Descripción: "Un cliente agrega 3 items, luego quita 1, el total de la compra es 300 y paga en Efectivo 350"
Caso de Prueba: "Agregar 3 items con el módulo de compras, luego quitar uno, luego calcular el total a pagar, asegurarse que sea 300 y utilizar el método en efectivo, el cliente paga 350 por lo que debe haber cambio"
Resultado Esperado: "Resultado Pago, el tipo debe ser "En Efectivo", fue_exitoso debe ser true y cambio debe ser 50.0"
El código en tests/integration_tests.rs quedará de la siguiente forma:
// tests/integration_tests.rs
use caja_supermercado_multiarchivos::models::modelo_compras;
use caja_supermercado_multiarchivos::models::modelo_pagos;
use caja_supermercado_multiarchivos::operaciones::compras;
use caja_supermercado_multiarchivos::operaciones::pagos;
// Es importante tomar en cuenta que
// como ahora estamos en otro folder que no es un modulo
// entonces no es necesario utilizar el mod tests o el
// #[cfg(test)]
#[test] // basta con agregar el macro tests
fn cliente_paga_con_tarjeta_de_credito() {
// Primero vamos a crear nuestro array de items
let mut items_compra: Vec<modelo_compras::Item> = Vec::new();
// Ahora vamos a agregar los 2 items
let item1: modelo_compras::Item = modelo_compras::Item {
nombre: String::from("item 1"),
precio_unitario: 50.00,
cantidad: 1.00
};
let item2: modelo_compras::Item = modelo_compras::Item {
nombre: String::from("item 2"),
precio_unitario: 150.00,
cantidad: 2.00
};
// Agregando los items
compras::agregar_item(&mut items_compra, item1);
compras::agregar_item(&mut items_compra, item2);
// Aca hacemos las primeras verificacione por ejemplo
// verificamos que en efecto haya unicamente dos items
assert_eq!(items_compra.len(), 2);
// si queremos podemos probar que el total es 350.00
assert_eq!(compras::total_compra(&items_compra), 350.00);
// Ahora probamos en pago como dice el caso de prueba
let resultado_pago = pagos::pagar(
modelo_pagos::MetodoDePago::Tarjeta, // metodo de pago
compras::total_compra(&items_compra), // total a pagar
350.00, // recibido del cliente
"12345" // numero de tarjeta
);
let resultado_esperado = modelo_pagos::ResultadoPago {
metodo_pago: String::from("Tarjeta"),
fue_exitoso: true,
cambio: 0.0
};
// Ahora verificamos que los resultados del pago sean los esperados
assert_eq!(resultado_pago.metodo_pago, resultado_esperado.metodo_pago);
assert_eq!(resultado_pago.fue_exitoso, resultado_esperado.fue_exitoso);
assert_eq!(resultado_pago.cambio, resultado_esperado.cambio);
}
#[test]
fn cliente_paga_en_efectivo_y_recibira_cambio() {
// Primero vamos a crear nuestro array de items
let mut items_compra: Vec<modelo_compras::Item> = Vec::new();
// Ahora vamos a agregar los 2 items
let item1: modelo_compras::Item = modelo_compras::Item {
nombre: String::from("item 1"),
precio_unitario: 50.00,
cantidad: 1.00
};
let item2: modelo_compras::Item = modelo_compras::Item {
nombre: String::from("item 2"),
precio_unitario: 150.00,
cantidad: 1.00
};
let item3: modelo_compras::Item = modelo_compras::Item {
nombre: String::from("item 3"),
precio_unitario: 150.00,
cantidad: 1.00
};
// Agregando los items
compras::agregar_item(&mut items_compra, item1);
compras::agregar_item(&mut items_compra, item2);
compras::agregar_item(&mut items_compra, item3);
// Aca hacemos las primeras verificacione por ejemplo
// verificamos que en efecto haya unicamente tres items
assert_eq!(items_compra.len(), 3);
// Ahora vamos a quitar un item y vamos a verificar que el total de compra sean 300.00
compras::quitar_item(&mut items_compra, 0); // quitando el primer item (item1)
assert_eq!(items_compra.len(), 2); // solo quedan 2 items
// probar que el total es 300.00
assert_eq!(compras::total_compra(&items_compra), 300.00);
// Ahora probamos en pago como dice el caso de prueba
let resultado_pago = pagos::pagar(
modelo_pagos::MetodoDePago::Efectivo, // metodo de pago
compras::total_compra(&items_compra), // total a pagar
350.00, // recibido del cliente
"" // numero de tarjeta
);
let resultado_esperado = modelo_pagos::ResultadoPago {
metodo_pago: String::from("En Efectivo"),
fue_exitoso: true,
cambio: 50.0
};
// Ahora verificamos que los resultados del pago sean los esperados
assert_eq!(resultado_pago.metodo_pago, resultado_esperado.metodo_pago);
assert_eq!(resultado_pago.fue_exitoso, resultado_esperado.fue_exitoso);
assert_eq!(resultado_pago.cambio, resultado_esperado.cambio);
}
Si ejecutamos nuevamente los tests vamos a ver que el resultado en consola ahora dirá que se ejecutaron dos pruebas de integración:
cargo test
Desde acá ya podemos crear todas las pruebas de integración que sean necesarias 🥳
El ejemplo completo puedes encontrarlo en el mismo repositorio pero en la rama version-con-pruebas-de-integracion.
Sin embargo, ahora podemos comenzar a realizarnos algunas preguntas como por ejemplo ¿cuánto de nuestro código realmente ha sido probado? ¿es posible realmente probar todo el código o hay bloques de códigos muy complicados de probar?, y muchas más, pero estas preguntas las vamos a contestar en el siguiente y último artículo de nuestra mini serie sobre pruebas con Rust 🧐
Si te ha gustado este artículo o te ha sido de utilidad, compártelo con tus amigos o en tus redes sociales 😁
Deja en la caja de comentarios los temas que te gustaría aprendamos sobre Rust y no te olvides seguirme en twitter
println!("Hasta la próxima!");