Una kata TDD (incluye código) en Rust

Una kata TDD (incluye código) en Rust

En el vibrante mundo del desarrollo de software, la creación de código robusto, mantenible y fiable es una prioridad ineludible. Entre las metodologías que nos asisten en esta misión, el Desarrollo Orientado a Pruebas (TDD, por sus siglas en inglés) se erige como una de las más potentes. Pero, ¿qué sucede cuando combinamos la disciplina de TDD con la potencia y seguridad sin concesiones de Rust? El resultado es una sinergia que puede transformar radicalmente la forma en que construimos nuestras aplicaciones. Este post no solo explorará los fundamentos de una kata TDD, sino que también nos sumergiremos en una implementación práctica utilizando Rust, desvelando cómo este binomio puede elevar la calidad de nuestro código a nuevos niveles. Prepárense para un viaje donde la seguridad en tiempo de compilación de Rust se une a la seguridad en tiempo de ejecución que nos brindan unas pruebas bien diseñadas.

¿Qué es una kata de código y por qué en Rust?

grey concrete wall Antes de sumergirnos en el código, es fundamental comprender qué significa embarcarse en una "kata de código" y por qué Rust es un compañero excepcional para esta práctica.

¿Qué es una kata de código?

El término "kata" proviene de las artes marciales japonesas, donde se refiere a una secuencia de movimientos o ejercicios que se practican repetidamente para perfeccionar una técnica. En el contexto del desarrollo de software, una "kata de código" es precisamente eso: un ejercicio de programación pequeño y autocontenido que se realiza una y otra vez para mejorar habilidades específicas. El objetivo no es tanto resolver el problema de la manera más eficiente o elegante la primera vez, sino más bien internalizar un proceso, ya sea el TDD, la refactorización, el uso de un nuevo lenguaje o paradigma, o la aplicación de patrones de diseño. Las katas de código son una herramienta invaluable para: * **Aprender y practicar TDD:** Permiten experimentar el ciclo "Rojo-Verde-Refactorizar" en un entorno de bajo riesgo. * **Explorar un nuevo lenguaje:** Ayudan a familiarizarse con la sintaxis, las bibliotecas estándar y las particularidades de un lenguaje (como el sistema de propiedad de Rust). * **Mejorar habilidades de refactorización:** Ofrecen oportunidades para reorganizar el código sin miedo, sabiendo que las pruebas están ahí para salvaguardar la funcionalidad. * **Desarrollar un "sentido" para el diseño de software:** Al resolver el mismo problema de diferentes maneras, se empieza a discernir qué enfoques conducen a un código más limpio y modular.

Ventajas de TDD con Rust

La combinación de TDD y Rust es, en mi opinión, una de las parejas más poderosas en el desarrollo moderno. Rust, con su enfoque en la seguridad, el rendimiento y la concurrencia, ya ofrece una base sólida para construir software de alta calidad. Cuando se integra TDD, estos beneficios se magnifican: * **Retroalimentación instantánea del compilador:** Rust tiene un compilador notoriamente estricto. Si bien esto puede ser frustrante al principio, en el contexto de TDD, se convierte en un aliado formidable. Los errores que otras lenguas permitirían en tiempo de ejecución, Rust los detecta en tiempo de compilación. Las pruebas unitarias de TDD complementan esto, verificando la lógica de negocio que el compilador no puede inferir. Es como tener dos niveles de validación constantes. * **Refactorización con confianza:** El sistema de tipos robusto de Rust y su modelo de propiedad (ownership) significan que, una vez que el código compila y las pruebas pasan, la probabilidad de introducir errores durante la refactorización se reduce drásticamente. Las pruebas unitarias garantizan la corrección funcional, mientras que el compilador asegura la corrección estructural y la seguridad de la memoria. Esto permite realizar cambios profundos con una confianza que es difícil de replicar en otros lenguajes. * **Diseño más limpio y modular:** TDD fomenta la escritura de funciones pequeñas, con responsabilidades claras y bien definidas, porque es más fácil probar unidades de código aisladas. Esta filosofía se alinea perfectamente con la mentalidad de Rust de construir componentes componibles y eficientes. * **Manejo de errores explícito:** Rust promueve el manejo explícito de errores a través de los tipos `Result` y `Option`. Las katas TDD brindan un excelente campo de juego para practicar cómo escribir pruebas que validen tanto los caminos de éxito como los de error, asegurando que el programa se comporte de manera predecible en todas las circunstancias. * **Framework de pruebas integrado:** Rust viene con un framework de pruebas incorporado (gracias a Cargo), lo que facilita enormemente la escritura y ejecución de pruebas. No hay necesidad de dependencias externas complejas para empezar con TDD.

Preparando el entorno para nuestra kata

Antes de escribir la primera línea de código de nuestra kata, necesitamos asegurarnos de que nuestro entorno de desarrollo de Rust esté configurado correctamente. Lo primero y más importante es tener Rust instalado. La forma recomendada de instalar Rust es a través de `rustup`, una herramienta que gestiona las versiones de Rust y las herramientas asociadas. Si aún no lo tienes, puedes seguir las instrucciones en el sitio oficial de Rust: Instalar Rust con rustup. Una vez que `rustup` esté instalado, puedes verificar tu instalación ejecutando `rustc --version` y `cargo --version` en tu terminal. Cargo es el gestor de paquetes y el sistema de construcción de Rust, y será nuestra herramienta principal para crear el proyecto y ejecutar las pruebas. Para iniciar nuestra kata, crearemos un nuevo proyecto de biblioteca de Rust. Una biblioteca es ideal para este tipo de ejercicios, ya que nos permite enfocarnos en la lógica de negocio sin preocuparnos por una estructura de aplicación más grande. ```bash cargo new --lib string_calculator_kata cd string_calculator_kata ``` Esto creará una nueva carpeta llamada `string_calculator_kata` con la siguiente estructura básica: ``` string_calculator_kata/ ├── Cargo.toml └── src/ └── lib.rs ``` El archivo `Cargo.toml` contiene los metadatos de nuestro proyecto y sus dependencias, mientras que `src/lib.rs` es donde escribiremos nuestro código principal y nuestras pruebas.

La kata: Calculadora de cadenas (String calculator)

Para nuestra kata, utilizaremos un clásico del TDD: la "Calculadora de cadenas" (String Calculator). Esta kata, popularizada por Roy Osherove, es excelente para practicar el ciclo TDD y explorar cómo agregar funcionalidad incrementalmente mientras se mantiene un código limpio y probado.

Definición del problema

El objetivo es crear una función `add` que acepte una cadena de texto como entrada y devuelva la suma de los números que contiene. Las reglas se introducirán de forma iterativa, tal como lo haríamos en un desarrollo real guiado por TDD. Las reglas básicas son: 1. Una cadena vacía debe devolver `0`. 2. Un solo número en la cadena debe devolver ese número. 3. Dos números separados por una coma deben devolver su suma. 4. La función `add` debe permitir manejar tanto comas como saltos de línea como delimitadores. 5. Los números negativos no están permitidos. Si aparecen, la función debe lanzar una excepción (o en Rust, retornar un `Result::Err` o hacer `panic!`) indicando todos los números negativos encontrados. 6. Los números mayores de 1000 deben ser ignorados (por ejemplo, "1001,2" debería devolver 2). 7. Soporte para delimitadores personalizados. La cadena puede comenzar con "//[delimitador]\n" para definir un delimitador de un solo carácter. Por ejemplo, "//;\n1;2" debe devolver 3. Como desarrolladores que adoptan el TDD, no implementaremos todas estas reglas de golpe. En su lugar, abordaremos cada regla con el ciclo "Rojo-Verde-Refactorizar".

Aplicando TDD paso a paso

Ahora, entremos en el corazón de nuestra kata. Abriremos el archivo `src/lib.rs` y comenzaremos a implementar nuestra función `add` y sus pruebas. ```rust // src/lib.rs // Definimos la función add aquí pub fn add(numbers: &str) -> i32 { // Implementación inicial muy básica para pasar el primer test 0 } #[cfg(test)] mod tests { use super::*; // Aquí irán nuestras pruebas } ```

Paso 1: Cadena vacía

**Rojo (Escribir una prueba que falle):** El requisito es que una cadena vacía devuelva `0`. ```rust // En src/lib.rs, dentro de mod tests { ... } #[test] fn empty_string_should_return_zero() { assert_eq!(add(""), 0); } ``` Ahora, ejecuta las pruebas con `cargo test`. Debería fallar, porque nuestra función `add` actual siempre devuelve `0`, lo cual pasa la prueba, pero esto es un *falso positivo* porque la lógica no está completa. Vamos a simular que la función aún no existe o devuelve otra cosa para ver el rojo. Pero como hemos puesto un `0` en la implementación, este test pasaría. La clave es que la implementación *sólo haga lo mínimo para pasar el test*. **Verde (Hacer que la prueba pase con la mínima implementación):** Nuestra implementación actual ya hace esto. ```rust // En src/lib.rs pub fn add(numbers: &str) -> i32 { 0 // Implementación mínima para pasar la prueba de cadena vacía } ``` `cargo test` ahora pasaría. **Refactorizar:** En este punto, no hay mucho que refactorizar. El código es demasiado simple.

Paso 2: Un número

**Rojo:** Un solo número en la cadena debe devolver ese número. ```rust // En src/lib.rs, dentro de mod tests { ... } #[test] fn single_number_should_return_itself() { assert_eq!(add("1"), 1); assert_eq!(add("5"), 5); } ``` `cargo test` debería fallar ahora, ya que `add("1")` devolvería `0` en nuestra implementación actual. **Verde:** Necesitamos parsear la cadena para obtener el número. ```rust // En src/lib.rs pub fn add(numbers: &str) -> i32 { if numbers.is_empty() { 0 } else { numbers.parse().unwrap_or(0) // Usamos unwrap_or(0) para simplificar por ahora } } ``` `cargo test` ahora debería pasar. **Refactorizar:** El `unwrap_or(0)` no es ideal si la cadena no es un número válido. Para esta kata, asumiremos entradas válidas, pero en un contexto real, manejaríamos el `Result` adecuadamente. Aquí no hay refactorización de estructura significativa todavía.

Paso 3: Dos números (delimitador coma)

**Rojo:** Dos números separados por una coma deben devolver su suma. ```rust // En src/lib.rs, dentro de mod tests { ... } #[test] fn two_numbers_comma_separated_should_return_sum() { assert_eq!(add("1,2"), 3); assert_eq!(add("5,7"), 12); } ``` `cargo test` debería fallar. Nuestra función solo puede manejar un número. **Verde:** Necesitamos dividir la cadena por la coma, parsear cada parte y sumar. ```rust // En src/lib.rs pub fn add(numbers: &str) -> i32 { if numbers.is_empty() { 0 } else { numbers .split(',') // Dividimos por la coma .map(|s| s.parse::
Diario Tecnología