03 · Modelar errores y escribir pruebas#
Objetivo#
Al terminar esta lección podrás modelar la ausencia de datos y los escenarios de fallo como ciudadanos de primera clase en el sistema de tipos utilizando Option<T> y Result<T, E>. Comprenderás por qué Rust prescinde por completo de los punteros nulos y de las excepciones invisibles en tiempo de ejecución, cómo el operador ? simplifica la propagación de fallos de forma segura y cómo diseñar una suite de pruebas unitarias exhaustiva bajo la disciplina RED arrow GREEN arrow REFACTOR. La meta es que tus tipos hagan que los estados inválidos sean irrepresentables y que cada aserción en tus pruebas actúe como un teorema verificable sobre las invariantes de tu dominio.
Antes de empezar#
Esta lección asume que comprendes tipos escalares, expresiones y contratos de funciones (01 · Tipos y flujo de control), así como ownership, préstamos y slices (02 · Ownership y préstamos). Utilizaremos exclusivamente la biblioteca estándar de Rust (std) y el arnés de pruebas integrado:
- Para compilar y ejecutar pruebas:
rustc --test archivo.rs
Si encuentras términos técnicos nuevos, consulta en cualquier momento el glosario técnico.
Diccionario de conceptos de la lección (Glosario desde cero)#
Para cimentar una comprensión rigurosa desde primeros principios, analizamos cada término técnico bajo cuatro dimensiones: intuición cotidiana, definición técnica formal, ejemplo concreto en código y concepción errónea frecuente.
1. Tipo de Dato Algebraico (ADT) / Tipo Suma (enum)#
- Intuición cotidiana: Una navaja suiza multifuncional: en cada momento exacto solo puedes tener desplegada una única herramienta específica (o bien la cuchilla, o bien las tijeras, o bien el destornillador), pero nunca dos al mismo tiempo ni ninguna de ellas a medias.
- Definición técnica precisa: Tipo compuesto que representa una unión disjunta de variantes mutuamente excluyentes (conocido formalmente en teoría de tipos como tipo suma). En Rust se declara con la palabra clave
enum. Cada instancia de unenumcontiene exactamente una de sus posibles variantes, representada en memoria por un discriminante numérico entero (tag) y suficiente espacio de almacenamiento para alojar los datos de la variante más grande (payload). - Ejemplo en código:
enum ConnectionState {
Disconnected,
Connecting,
Connected,
}
- Concepción errónea: Pensar que un
enumen Rust es una simple lista de constantes numéricas como en C, C++ o Java clásico. En Rust, unenumes un tipo de dato algebraico completo que puede encapsular datos heterogéneos y tuplas o estructuras complejas dentro de cada variante.
2. Variante con datos asociados (Payload)#
- Intuición cotidiana: Un paquete postal con etiqueta exterior: la etiqueta dice "Frágil" y dentro contiene una taza de cerámica envuelta; otro paquete dice "Urgente" y dentro contiene un documento confidencial. El tipo de contenido varía según la etiqueta exterior.
- Definición técnica precisa: Variante de un
enumque contiene campos o tipos de datos adicionales adjuntos a su discriminante. Esto permite que diferentes variantes de un mismo tipo alberguen cargas útiles completamente distintas. - Ejemplo en código:
enum WebhookEvent {
Ping,
MessageReceived { user_id: u32, content: String },
ConnectionFailed(u16), // Código de estado HTTP
}
- Concepción errónea: Creer que las variantes con datos asociados implican polimorfismo dinámico o llamadas virtuales costosas (vtable). El compilador calcula estáticamente el tamaño máximo necesario y alinea la estructura en una única posición contigua de memoria.
3. Option<T> (Some, None)#
- Intuición cotidiana: Una caja de regalo sorpresa: al abrirla, o bien encuentras un regalo valioso (
Some(regalo)), o bien encuentras que la caja está totalmente vacía (None). Nunca puedes usar el regalo sin abrir primero la caja para comprobar si hay algo dentro. - Definición técnica precisa: Tipo de dato algebraico estándar definido como:
enum Option<T> {
None,
Some(T),
}
Modela formalmente la presencia o ausencia de un valor de tipo T. En Rust, una variable de tipo T jamás puede ser nula. Si un valor puede no existir, el tipo debe ser obligatoriamente Option<T>.
- Ejemplo en código:
fn find_user_id(username: &str) -> Option<u32> {
if username == "admin" {
Some(1)
} else {
None
}
}
- Concepción errónea: Tratar a
Nonecomo un sustituto directo del puntero nulo (nullonil). En otros lenguajes, cualquier variable puede contenernullsilenciosamente y colapsar en ejecución. En Rust, el tipoTes seguro por defecto; no puedes acceder al contenido de unOption<T>sin desempaquetarlo explícitamente mediante coincidencia de patrones (pattern matching) o combinadores.
4. Result<T, E> (Ok, Err)#
- Intuición cotidiana: Un informe de laboratorio clínico: el resultado final es o bien un diagnóstico con éxito que contiene tus datos analizados (
Ok(datos)), o bien un informe de anomalía que explica detalladamente por qué la muestra no se pudo procesar (Err(causa)). - Definición técnica precisa: Tipo de dato algebraico estándar definido como:
enum Result<T, E> {
Ok(T),
Err(E),
}
Modela el resultado de una operación que puede concluir con éxito produciendo un valor de tipo T, o fracasar produciendo un error recuperable tipado de tipo E.
- Ejemplo en código:
fn divide(numerator: u32, denominator: u32) -> Result<u32, &'static str> {
if denominator == 0 {
Err("division by zero is undefined")
} else {
Ok(numerator / denominator)
}
}
- Concepción errónea: Pensar que
Resultoculta las rutas de fallo como los bloquestry/catchde las excepciones. ConResult, el potencial de fallo está escrito explícitamente en el contrato de tipo de la función y debe ser gestionado por el llamador.
5. Operador de propagación ? y retorno anticipado (Early Return)#
- Intuición cotidiana: Un empleado de aduana que revisa una fila de documentos: si el sello del primer documento es válido, lo abre y pasa al siguiente sello; si un solo sello es inválido, detiene el proceso de inmediato y te devuelve el expediente con el rechazo sellado sin perder tiempo revisando el resto.
- Definición técnica precisa: Operador sufijo unario (
expr?) aplicable sobre expresiones que evalúan aResult<T, E>(uOption<T>). Si la expresión esOk(v), la desenvuelve extrayendo el valor internovy el flujo continúa linealmente. Si la expresión esErr(e), detiene inmediatamente la ejecución de la función actual y ejecuta un retorno anticipado devolviendoErr(From::from(e))al llamador. - Ejemplo en código:
fn parse_and_double(input: &str) -> Result<u32, std::num::ParseIntError> {
let value = input.trim().parse::<u32>()?; // Si falla, retorna Err de inmediato
Ok(value * 2)
}
- Concepción errónea: Creer que
?"atrapa" o suprime el error. El operador?no oculta ni silencia el fallo: automatiza el patrón repetitivo de retorno anticipado devolviendo el error a la capa superior.
6. Combinador .map_err()#
- Intuición cotidiana: Un traductor simultáneo en una embajada: cuando el departamento técnico emite un informe con terminología interna compleja, el traductor lo reformula en una categoría diplomática oficial comprensible para el embajador.
- Definición técnica precisa: Método de orden superior implementado en
Result<T, E>con firma:
fn map_err<F, O>(self, op: O) -> Result<T, F>
where
O: FnOnce(E) -> F
Si el resultado es Ok(v), deja el valor intacto. Si es Err(e), aplica la clausura transformadora op(e) para convertir el tipo de error original E a un nuevo tipo de error de dominio F.
- Ejemplo en código:
let result: Result<u32, String> = "abc"
.parse::<u32>()
.map_err(|e| format!("Parsing failed due to: {e}"));
- Concepción errónea: Pensar que
.map_err()detiene la ejecución. Es una transformación funcional pura que cambia el tipo de dato alojado dentro de la varianteErrsin alterar el flujo de control.
7. Pánico (panic!) vs Error recuperable#
- Intuición cotidiana:
- Error recuperable: El cajero automático te informa que tu tarjeta tiene fondos insuficientes; el sistema sigue encendido y te permite intentar retirar un monto menor.
- Pánico (
panic!): El cajero automático sufre un corte eléctrico súbito o un cortocircuito en el dispensador de billetes; la máquina no puede garantizar la integridad física y se apaga de emergencia. - Definición técnica precisa:
- Error recuperable (
Result<T, E>): Condición esperable del flujo ordinario del sistema (por ejemplo: entrada de usuario mal formada, archivo no encontrado, red no disponible). Se gestiona y procesa programáticamente en tiempo de ejecución. - Pánico (
panic!): Mecanismo de fallo catastrófico utilizado cuando se ha violado una invariante fundamental de memoria o lógica de programación imposible de recuperar (por ejemplo: acceso fuera de límites en un arreglov[10], desreferenciación enunsafecorrupto, o agotamiento crítico de recursos). Por defecto, inicia el desenrollado de la pila (stack unwinding) o aborta el proceso inmediatamente. - Ejemplo en código:
// Error recuperable: Devuelve Result al llamador
fn read_config(path: &str) -> Result<String, std::io::Error> { ... }
// Pánico: Invariante rota que jamás debería ocurrir
if internal_state.is_corrupted() {
panic!("Critical kernel state violation");
}
- Concepción errónea: Usar
panic!o.unwrap()como mecanismo habitual de control de flujo en APIs públicas o servidores backend. Unpanic!no controlado en un hilo de trabajo puede tumbar conexiones o degradar la disponibilidad del servicio.
8. Traits derivados #[derive(Debug, PartialEq, Eq)]#
- Intuición cotidiana: Un sello de certificación que añade automáticamente capacidades estándar a un documento: capacidad de ser leído e impreso para inspección visual (
Debug) y capacidad de ser comparado con otro documento para ver si son idénticos (PartialEq). - Definición técnica precisa: Macro procedural derivativa que genera de forma automática la implementación canónica de traits de la biblioteca estándar:
Debug: Permite formatear el valor para inspección de depuración utilizando el especificador{:?}.PartialEq: Implementa el operador de igualdad binaria==y desigualdad!=.Eq: Marca de trait que garantiza que la relación de equivalencia es reflexiva (a == a es siempre verdadero, a diferencia de los números en punto flotante dondeNaN != NaN).- Ejemplo en código:
#[derive(Debug, PartialEq, Eq)]
enum Status {
Active,
Inactive,
}
- Concepción errónea: Creer que las aserciones de prueba como
assert_eq!(a, b)funcionan con cualquier tipo por arte de magia. Si tu tipo no implementaPartialEq(para comparar) yDebug(para imprimir la diferencia si la prueba falla), el compilador rechazará la aserción con un error de compilación.
9. Fixture de prueba (Test Fixture)#
- Intuición cotidiana: El banco de pruebas calibrado en un laboratorio automotriz: un entorno con temperatura controlada, dinamómetro y combustible estándar para medir las emisiones del motor bajo condiciones conocidas y reproducibles.
- Definición técnica precisa: Conjunto fijo de datos, configuraciones, estados iniciales y dependencias preparadas previamente para ejecutar una prueba automatizada con el fin de asegurar que el resultado sea enteramente determinista y reproducible.
- Ejemplo en código:
fn setup_test_data() -> Vec<String> {
vec![String::from("alpha"), String::from("beta")]
}
- Concepción errónea: Pensar que las pruebas deben conectarse siempre a sistemas externos en vivo (como bases de datos de producción o pasarelas de pago reales). Una prueba unitaria sólida utiliza fixtures controlados y aislados en memoria.
Modelo mental y desempacado paso a paso#
1. ¿Por qué Rust erradica los punteros nulos y las excepciones invisibles?#
En 1965, Sir Tony Hoare inventó la referencia nula (null) para el lenguaje ALGOL W. Décadas más tarde, en una conferencia en 2009, se disculpó públicamente calificándolo como su "error de los mil millones de dólares" (The Billion-Dollar Mistake):
"Lo llamo mi error de mil millones de dólares... En aquel momento estaba diseñando el primer sistema de tipos completo para referencias en un lenguaje orientado a objetos... Simplemente no pude resistir la tentación de introducir una referencia nula, simplemente porque era muy fácil de implementar. Esto ha provocado innumerables errores, vulnerabilidades y fallos en sistemas durante los últimos cuarenta años."
En lenguajes tradicionales (como Java, C#, C++, Python, JavaScript):
- La presencia de
nulles un peligro silencioso: Cualquier variable de tipo objeto puede contenernull. El desarrollador no puede saber con certeza si una función le entregará un dato válido o un puntero vacío sin leer la documentación o revisar el código fuente. Una comprobación omitida resulta en un colapso en producción (NullPointerException,TypeError: cannot read properties of undefined). - Las excepciones son saltos invisibles de control: Cuando invocas
user.save(), el método puede lanzar una excepción de red, de base de datos o de validación. La firma del método no declara estos fallos en su tipo de retorno; el compilador no te obliga a capturarla. El flujo del programa salta de forma impredecible a través del árbol de llamadas, saltándose limpiezas de recursos a menos que se usen bloquestry/finallymeticulosamente diseñados.
La solución de Rust:
- No existe el valor
nullninil. - No existen excepciones en tiempo de ejecución para errores ordinarios.
- La posibilidad de ausencia es una propiedad explícita del tipo: si un valor puede no existir, su tipo es
Option<T>. - La posibilidad de fallo es una propiedad explícita del tipo: si una operación puede fallar, su tipo es
Result<T, E>. - El compilador no permite utilizar el valor interior
Tsin que el código contemple formalmente ambos escenarios (Some/NoneoOk/Err). El olvido es imposible a nivel de compilación.
2. Anatomía y desazucarización (Desugaring) del operador ?#
El operador ? es azúcar sintáctico sobre una expresión match con retorno anticipado. Cuando escribes:
let value = fallible_action()?;
El compilador de Rust desazucara exactamente esa instrucción en el siguiente bloque lógico:
let value = match fallible_action() {
Ok(val) => val,
Err(err) => return Err(From::from(err)),
};
Aspectos cruciales a comprender:#
- *Extracción directa en el camino feliz (Happy Path): Si la llamada tiene éxito (
Ok), el valor internovalse asigna directamente a la variablevaluesin anidamientos ni sangrías innecesarias (pyramid of doom*). - *Retorno anticipado (Early Return):* Si la llamada falla (
Err), la función en la que te encuentras termina inmediatamente en ese renglón devolviendo la variante de error. - Conversión automática de tipos con
From::from: El operador?no exige que el error de la suboperación sea idéntico al tipo de error retornado por la función exterior. Solo exige que el tipo de error exterior implemente el traitFrom<ErrorInterior>. Si la conversión existe, Rust adapta el error automáticamente.
3. ¿Por qué assert_eq! exige #[derive(Debug, PartialEq)]?#
La macro assert_eq!(left, right) es el corazón de las pruebas en Rust. Si intentas comparar dos instancias de un enum o struct creado por ti sin estas anotaciones, la compilación fallará. ¿Por qué?
La macro assert_eq! se expande aproximadamente en:
match (&(left), &(right)) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
panic!(
"assertion failed: `(left == right)`\n left: `{:?}`,\n right: `{:?}`",
&*left_val, &*right_val
);
}
}
}
- La comparación
*left_val == *right_valrequiere que el tipo implemente el traitPartialEq. Sin él, el operador==no está definido para ese tipo en el lenguaje. - El formateo de fallo
left: {:?}requiere que el tipo implemente el traitDebug. Si la aserción falla, el arnés de prueba debe imprimir en la consola qué valor específico tenía cada lado para que el ingeniero pueda diagnosticar la causa.
4. Errores tipados con enum vs cadenas de texto crudas (String)#
Cuando una función en el límite de un módulo falla, un programador novato podría verse tentado a retornar Result<T, String>:
// Antiprón: Error como cadena de texto cruda
fn parse_id(s: &str) -> Result<u32, String> {
if s.is_empty() {
return Err(String::from("input cannot be empty"));
}
s.parse::<u32>().map_err(|e| e.to_string())
}
¿Por qué esto degrada la arquitectura del sistema?#
- Asignación innecesaria en el Heap: Cada error reserva dinámicamente memoria para almacenar la cadena de texto, introduciendo sobrecoste en rutas de alta frecuencia.
- Pérdida de semántica para el llamador: Si la capa superior (por ejemplo, un controlador HTTP) necesita distinguir si el error fue "entrada vacía" (para devolver un HTTP 400 Bad Request) o "usuario no encontrado" (para devolver un HTTP 404 Not Found), con un
Stringtendría que hacer una frágil comparación de texto (if err == "input cannot be empty"). Si el mensaje se traduce o cambia una coma, el software se rompe silenciosamente. - Diseño idiomático con
enum: ``rust #[derive(Debug, PartialEq, Eq)] enum ParseIdError { Empty, InvalidFormat, }`Al usar unenum, el llamador puede usar un bloquematch` exhaustivo garantizado por el compilador, sin asignaciones de memoria y con cero fragilidad textual.
Diagrama#
flowchart TD
A[Entrada] --> B{Validación}
B -->|válida| C[Ok valor]
B -->|inválida| D[Err causa]
C --> E[El llamador usa el éxito]
D --> F[El llamador decide respuesta]
El diagrama separa modelar el fallo de decidir cómo presentarlo: la capa HTTP decidirá después si un error corresponde a 400, 404 u otra respuesta.
Ejemplo razonado#
El siguiente bloque es un programa completo y ejecutable con pruebas, basado exclusivamente en la biblioteca estándar de Rust. Guárdalo como main.rs y pruébalo con rustc --test main.rs:
// Domain Error: Categorizes the possible failure modes when parsing a numeric count.
#[derive(Debug, PartialEq, Eq)]
enum ParseCountError {
Empty,
InvalidNumber,
}
// Contract: Parses a text slice into an unsigned 32-bit count.
// Whitespace is trimmed from both sides.
// Returns Err(ParseCountError::Empty) if the trimmed input is empty.
// Returns Err(ParseCountError::InvalidNumber) if the input cannot be parsed as u32.
fn parse_count(input: &str) -> Result<u32, ParseCountError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(ParseCountError::Empty);
}
trimmed
.parse::<u32>()
.map_err(|_| ParseCountError::InvalidNumber)
}
#[cfg(test)]
mod tests {
use super::{parse_count, ParseCountError};
#[test]
fn distinguishes_empty_invalid_and_valid_inputs() {
// Case 1: Whitespace-only string must return Empty
assert_eq!(parse_count(" "), Err(ParseCountError::Empty));
// Case 2: Non-numeric alphabetic input must return InvalidNumber
assert_eq!(parse_count("many"), Err(ParseCountError::InvalidNumber));
// Case 3: Valid numeric string must return Ok with the expected u32 value
assert_eq!(parse_count("12"), Ok(12));
// Case 4: Valid numeric string surrounded by whitespace must succeed
assert_eq!(parse_count(" 42 "), Ok(42));
}
}
Comentario línea por línea y análisis técnico:#
- Líneas 2-6: Definición del
enum ParseCountErrorcon#[derive(Debug, PartialEq, Eq)]. Permite que las variantes actúen como valores de error ricos y comparables en pruebas conassert_eq!. - Línea 12:
fn parse_count(input: &str) -> Result<u32, ParseCountError> - Firma clara y honesta: no oculta excepciones. El llamador sabe con certeza absoluta que la operación puede producir un entero
u32o una de las dos causas de error tipadas. - Línea 13:
let trimmed = input.trim(); trim()devuelve un slice&strque descarta espacios en blanco al inicio y al final. No reserva memoria en el heap; reutiliza el búfer de entrada.- Líneas 15-17: Verificación de frontera de dominio. Si la cadena tras el recorte queda vacía, se retorna anticipadamente
Err(ParseCountError::Empty). - Líneas 19-22:
trimmed.parse::<u32>()es un método estándar que intenta parsear el slice au32. Devuelve unResult<u32, std::num::ParseIntError>..map_err(|_| ParseCountError::InvalidNumber): Transforma el error técnico interno de la biblioteca estándar (ParseIntError) a nuestra variante de dominio tipada. Si la conversión tiene éxito, elOk(valor)pasa intacto y se devuelve como última expresión de la función.- Líneas 25-42: Módulo de pruebas
tests. Cubre exhaustivamente las cuatro clases de equivalencia de la entrada: vacía con espacios, texto no numérico, número válido y número rodeado de espacios.
Práctica independiente#
Ejercicio propuesto: Parser de prioridad de tareas#
Debes diseñar e implementar un analizador de prioridad para un sistema de gestión de tareas con la siguiente firma canónica:
pub fn parse_priority(input: &str) -> Result<u8, PriorityError>
Reglas y especificación de dominio:#
- Las prioridades válidas en el dominio están restringidas estrictamente al rango cerrado de 1 a 5 inclusive (1 ≤ prioridad ≤ 5).
- Los espacios en blanco iniciales o finales deben ser ignorados de forma transparente mediante
.trim(). - Si la entrada tras el recorte queda vacía, la función debe fallar indicando que no se recibió contenido.
- Si la entrada contiene caracteres no numéricos o excede la capacidad de un entero sin signo de 8 bits, debe fallar indicando formato numérico inválido.
- Si la entrada es un número válido pero queda fuera del intervalo [1, 5] (por ejemplo:
0,6,10), debe fallar informando que el valor está fuera de rango e incluyendo el valor numérico rechazado dentro de la variante del error para fines de diagnóstico.
Diseño del Enum de Error:#
Declara formalmente el enum de error con las siguientes variantes y traits derivados:
#[derive(Debug, PartialEq, Eq)]
pub enum PriorityError {
Empty,
InvalidNumber,
OutOfRange(u8),
}
Casos de prueba obligatorios para la matriz TDD (RED arrow GREEN arrow REFACTOR):#
Entrada (input) | Resultado esperado | Razón del caso de prueba |
|---|---|---|
" " | Err(PriorityError::Empty) | Cadena compuesta solo por espacios |
"" | Err(PriorityError::Empty) | Cadena totalmente vacía |
"urgent" | Err(PriorityError::InvalidNumber) | Texto alfabético no convertible |
"999" | Err(PriorityError::InvalidNumber) | Desbordamiento de u8 (> 255) |
"0" | Err(PriorityError::OutOfRange(0)) | Límite inferior violado (< 1) |
"1" | Ok(1) | Límite inferior válido |
" 3 " | Ok(3) | Valor central con espacios recortables |
"5" | Ok(5) | Límite superior válido |
"6" | Err(PriorityError::OutOfRange(6)) | Límite superior violado (> 5) |
Pasos de desarrollo guiado por pruebas:#
- Fase RED: Crea
priority.rscon elenum PriorityErrory la funciónparse_priorityretornando temporalmentetodo!(). Añade los 9 casos de prueba enmod tests. Ejecuta: ``bash rustc --test priority.rs`Comprueba que el test colapsa con pánico entodo!()`. Has establecido la evidencia del fallo inicial. - Fase GREEN: Implementa la lógica de
parse_priority: recorta la cadena, comprueba si está vacía, utiliza.parse::<u8>()?con.map_err(), y finalmente realiza unmatchsobre el entero resultante para clasificar1..=5frente a cualquier otro valor. Vuelve a ejecutarrustc --test priority.rshasta que todos los tests pasen exitosamente. - Fase REFACTOR: Verifica la claridad de la implementación. Asegúrate de que no existen desempaquetados inseguros (
.unwrap()o.expect()) en la función principal.
Preguntas de comprensión profunda y diagnóstico socrático#
Responde críticamente a estas cinco preguntas de razonamiento técnico antes de avanzar:
Pregunta 1: Desazucarización y tipo de retorno con ?#
Observa el siguiente intento de uso del operador ? dentro de una función simple:
fn print_user_age(input: &str) {
let age = input.trim().parse::<u32>()?;
println!("User age is: {age}");
}
- ¿Por qué este código es rechazado de inmediato por el compilador de Rust?
- ¿Qué requisito fundamental exige el operador
?sobre el tipo de retorno de la función que lo alberga? - ¿Cómo desazucara el compilador la línea del
parseen términos dematchy qué tipo intentaría devolver elreturn Err(...)implícito en este caso?
Pregunta 2: Sobrecoste en tiempo de ejecución: Excepciones vs Result#
En la máquina virtual de Java (JVM) o en el runtime de .NET (CLR):
- ¿Qué operaciones físicas de CPU y memoria ocurren cuando se lanza una excepción mediante
throw new InvalidInputException()(investiga sobre la captura de traza de pila o stack trace capture y el desenrollado de marcos de pila o stack unwinding)? - En contraste, ¿cuál es la representación física de un
Result<u8, PriorityError>en la pila de Rust y qué instrucciones de ensamblador genera el procesador al evaluar unOko unErr?
Pregunta 3: Requisitos de traits en el arnés de pruebas#
Considera este código de prueba:
enum MyCustomError { BadInput }
#[test]
fn test_failure() {
let result: Result<u32, MyCustomError> = Err(MyCustomError::BadInput);
assert_eq!(result, Err(MyCustomError::BadInput));
}
- Si intentas compilar este código tal como está, ¿cuáles son los dos errores de compilación específicos que emitirá
rustc? - Explica qué método concreto proporciona el trait
PartialEqy qué formato de impresión activa el traitDebugdentro de la macroassert_eq!.
Pregunta 4: La frontera arquitectónica entre Option, Result y panic!#
Imagina que estás construyendo una API web en Rust:
- ¿Bajo qué condiciones específicas una función de consulta a la base de datos debería devolver
Ok(None)en lugar de unErr(...)? - ¿Bajo qué circunstancias debería devolver un
Err(DatabaseError)? - Si la conexión a la base de datos reporta que la memoria compartida del driver C está irremediablemente corrompida, ¿por qué es justificado invocar un
panic!en lugar de retornar unResult?
Pregunta 5: Errores tipados con enum vs cadenas String#
Imagina una biblioteca cliente de pagos que expone una función process_payment(amount: u32) -> Result<TransactionId, String>.
- ¿Qué problema enfrentará un desarrollador consumidor si quiere mostrar en la interfaz de usuario un mensaje en español cuando falla por "saldo insuficiente" y un mensaje en inglés cuando falla por "tarjeta vencida"?
- ¿Cómo resuelve este problema el modelado mediante un
enum PaymentErrorcon variantes explícitas?
Code smell#
Síntoma: Desempaquetado indiscriminado (.unwrap()) en rutas de entrada de datos#
Un desarrollador recibe una cadena JSON o parámetros de consulta HTTP y escribe:
// Code smell: Invocación de unwrap en una frontera de datos externa
let user_id: u32 = request.query_param("user_id").unwrap().parse::<u32>().unwrap();
- Evidencia observable: Múltiples llamadas directas a
.unwrap()o.expect()sobre métodos que devuelvenOptionoResultal procesar datos provenientes de clientes, archivos o conexiones de red. - Hipótesis del problema: El desarrollador asume incorrectamente que el cliente HTTP siempre enviará un parámetro bien formado y presente, ignorando el contrato explícito del tipo.
- Consecuencia concreta: Un usuario malintencionado o un cliente desactualizado envía una petición sin el parámetro
user_ido con una letra en lugar de un número. El hilo de trabajo colapsa con unpanic!, abortando la petición y potencialmente saturando el servidor con caídas recurrentes (vulnerabilidad de denegación de servicio, DoS). - Tratamiento mínimo justificado: Propagar el error transformándolo a un
Resultcon un error de dominio tipado (RequestError::MissingParam,RequestError::InvalidFormat) y devolver una respuesta HTTP formal (como400 Bad Request) de manera controlada. - Cuándo SÍ está justificado usar
.unwrap():
- En código de pruebas unitarias (
#[test]), donde un fallo inmediato es precisamente lo deseado para que la prueba sea marcada como fallida. - Cuando una invariante previa en el código ha demostrado matemáticamente que el valor jamás puede ser
NoneoErr(por ejemplo:let static_regex = Regex::new("^[0-9]+$").unwrap();donde la expresión regular es una constante fija conocida en compilación y sin errores de sintaxis).
Criterio de salida#
Puedes considerar superada esta lección con maestría cuando:
- Puedes explicar con precisión matemática por qué
OptionyResultprevienen vulnerabilidades de puntero nulo y cómo el operador?desazucara en unmatchcon retorno anticipado. - Tu implementación de
parse_prioritypasa con éxito todos los casos de prueba de frontera (incluyendo cadenas vacías, valores fuera de rango0y6, y desbordamientos numéricos). - Puedes justificar técnicamente por qué
assert_eq!requiere que los tipos implementenPartialEqyDebug. - Has experimentado el ciclo TDD observando un fallo real (RED) antes de implementar el código que hace pasar la suite (GREEN).
Lecturas oficiales#
- Rust Book,
Resulty errores recuperables: El operador?y la propagación canónica de errores. - Rust Book, ¿Pánico o no pánico?: Criterios de diseño para decidir cuándo abortar y cuándo retornar
Result. - Rust Book, Escribir pruebas automatizadas: Anatomía de las pruebas unitarias y macros de aserción.
- Glosario técnico del curso: Consulta definiciones canónicas sobre tipos de datos algebraicos y combinadores de errores.
Siguiente paso#
Continúa con 04 · Colecciones e iteración, donde aprenderás a gestionar múltiples elementos en memoria mediante vectores dinámicos (Vec<T>) y tablas hash (HashMap<K, V>), dominando los tres modos de iteración y la evaluación perezosa de transformaciones sin sobrecoste de asignación.