wandres.dev
TESTING · unit, integration, doc, property

Doc tests: la documentación que se compila y no miente

Los ejemplos que escribes en los comentarios /// no son texto muerto: cargo test los extrae, los compila y los ejecuta como pruebas. Si tu documentación afirma algo, más vale que sea cierto, porque el compilador lo verifica. Con el envoltorio implícito de main, soporte para ?, líneas ocultas y atributos como no_run o compile_fail.

⏱ 18 min

En toda tu carrera has visto documentación que envejece hasta mentir: el ejemplo que ya no compila, la firma que cambió, el foo(x, y) que hoy pide tres argumentos. La documentación se pudre porque nada la ata a la verdad del código. Rust corta ese nudo con una idea tan simple como radical: los bloques de código que escribes dentro de los comentarios de documentación /// se compilan y se ejecutan como pruebas. Cuando lanzas cargo test, además de tus tests unitarios y de integración, rustdoc extrae cada ejemplo, lo envuelve en un programa, lo compila contra la versión actual de tu crate y lo corre. Si el ejemplo dejó de compilar, la suite falla. Si su assert_eq! ya no cuadra, la suite falla. La consecuencia es un tipo de documentación que la mayoría de lenguajes no puede ofrecer: una que no puede mentir, porque el compilador la vigila en cada commit.

🎯 Al terminar esta lección sabrás
  • Escribir ejemplos en /// que se compilen y ejecuten, verificando su comportamiento con assert_eq!.
  • Entender el envoltorio implícito de fn main y cómo usar el operador ? dentro de un doc test.
  • Ocultar líneas de preparación con # sin ensuciar la documentación que lee el usuario.
  • Controlar la ejecución con atributos: no_run, ignore, should_panic, compile_fail y text.

El ejemplo que se ejecuta

Un comentario /// documenta el elemento que le sigue. Dentro, un bloque cercado con triple acento grave y etiqueta rust se convierte en un doc test:

/// Duplica un entero.
///
/// # Ejemplos
///
/// ```
/// use micrate::duplica;
///
/// assert_eq!(duplica(21), 42);
/// ```
pub fn duplica(x: i32) -> i32 {
    x * 2
}

El bloque no es decorativo: cargo test lo compila como un pequeño programa que importa tu crate, ejecuta las líneas y comprueba el assert_eq!. Si mañana renombras duplica o cambias su firma, este ejemplo dejará de compilar y la suite te lo gritará antes de que un usuario tropiece con él. La etiqueta por defecto de un bloque sin lenguaje es rust, así que puede omitirse; para prosa que no debe compilarse, se usa la etiqueta text.

Una comodidad clave: rustdoc envuelve tu ejemplo en una fn main implícita. Por eso el fragmento anterior no la escribe. Y como ese main puede tener el tipo de retorno adecuado, los doc tests soportan el operador ? si el bloque termina devolviendo Ok:

/// Parsea un puerto desde texto.
///
/// ```
/// let puerto: u16 = "8080".parse()?;
/// assert_eq!(puerto, 8080);
/// # Ok::<(), std::num::ParseIntError>(())
/// ```
pub fn documentacion() {}

Líneas ocultas: el ejemplo limpio por fuera, completo por dentro

Un ejemplo debe compilar de verdad, lo que a menudo exige líneas de andamiaje —imports, una construcción previa, el Ok(()) final— que distraen al lector sin enseñarle nada. Rust resuelve esta tensión con el prefijo #: una línea del bloque que empieza por # se compila y ejecuta, pero no se muestra en la documentación renderizada.

/// Suma los elementos de un vector.
///
/// ```
/// # use micrate::suma_total;
/// let datos = vec![1, 2, 3, 4];
/// assert_eq!(suma_total(&datos), 10);
/// ```
pub fn suma_total(v: &[i32]) -> i32 {
    v.iter().sum()
}

Quien lee la documentación ve solo las dos líneas centrales, nítidas y didácticas; el compilador, en cambio, ve también el use oculto y verifica el conjunto completo. Es la conciliación perfecta entre dos audiencias que suelen estar en conflicto: el humano que quiere claridad y la máquina que exige exactitud.

Hay un dividendo que trasciende la corrección: estos ejemplos son también la cara pública de tu crate. rustdoc los renderiza en la documentación que se publica en docs.rs, de modo que el mismo fragmento que el compilador verifica es el primero que un usuario lee al evaluar tu librería. Un ejemplo que compila da confianza; uno que además ilustra el caso de uso central vende la API mejor que cualquier párrafo. En Rust, la frontera entre probar, documentar y persuadir se desdibuja hasta casi desaparecer.

⚠️
El // no documenta: el /// sí

Solo los comentarios de documentación generan doc tests. /// documenta el elemento siguiente; //! documenta el elemento contenedor (un módulo o el crate entero, desde su interior). Un comentario ordinario // es invisible para rustdoc y sus bloques de código nunca se ejecutan. Además, los doc tests solo existen para librerías: se prueban a través de la API pública, igual que un usuario, por lo que un crate puramente binario no los produce.

📎

/// documenta lo siguiente

Se adhiere al elemento que viene debajo: una función, un struct, un método. Su bloque de código se compila y se ejecuta.

📦

//! documenta el contenedor

Escrito desde dentro de un módulo o del crate, documenta el propio contenedor. Ideal para el ejemplo de portada del crate.

🙈

# oculta la línea

Se compila y ejecuta, pero no aparece en la documentación. El andamiaje invisible que hace el ejemplo correcto sin ensuciarlo.

🚫

compile_fail: lo prohibido

El ejemplo debe fallar al compilar. Ata a una prueba aquello que el sistema de tipos rechaza por diseño.

Atributos: gobernar qué se compila y qué se ejecuta

No todo ejemplo puede o debe ejecutarse. Un fragmento que abre una conexión de red compilaría, pero correrlo en CI sería frágil. Otro pretende ilustrar precisamente que algo no compila. Para eso, la valla del bloque acepta atributos tras la etiqueta:

  • no_run — se compila (garantiza que la firma es válida) pero no se ejecuta. Ideal para ejemplos con efectos de red, ficheros o bucles infinitos.
  • ignore — ni se compila ni se ejecuta. El último recurso; casi siempre no_run o text es una elección más honesta.
  • should_panic — el ejemplo debe entrar en pánico para considerarse aprobado.
  • compile_fail — el ejemplo debe fallar al compilar; si compilase, la prueba falla. Sirve para documentar que el sistema de tipos rechaza un mal uso.
  • text — el bloque es prosa, no Rust; no se toca.
/// Este uso viola el borrow checker y no debe compilar:
///
/// ```compile_fail
/// let mut v = vec![1, 2, 3];
/// let primero = &v[0];
/// v.push(4);          // prestamo mutable con uno inmutable vivo
/// println!("{primero}");
/// ```
pub fn ejemplo_negativo() {}

Puedes ejecutar únicamente estas pruebas con cargo test --doc. El atributo compile_fail es especialmente elegante: convierte una garantía de seguridad —“el compilador rechaza esto”— en una prueba que se rompería si una futura versión, por descuido, empezara a aceptarlo.

flowchart TD
D[Escribes un ejemplo en un comentario tres barras] --> E[rustdoc lo extrae al ejecutar cargo test]
E --> W[Lo envuelve en un main implicito con soporte para interrogacion]
W --> A{Que atributo lleva la valla}
A -->|por defecto| R[Compila y ejecuta y verifica las aserciones]
A -->|no_run| C[Compila pero no ejecuta]
A -->|compile_fail| N[Debe fallar al compilar]
A -->|text| T[Prosa que no se toca]
style D fill:#cba6f7,color:#11111b
style W fill:#89b4fa,color:#11111b
style R fill:#a6e3a1,color:#11111b
style N fill:#f38ba8,color:#11111b
Ejecutar la documentación es cerrar el bucle entre lo que dices y lo que haces

La mayoría de los lenguajes tratan el código y su documentación como dos textos paralelos que un ser humano promete mantener sincronizados —una promesa que la entropía incumple sin falta—. Rust rehúsa esa fe ciega y hace algo conceptualmente distinto: convierte la documentación en código verificable, cerrando el bucle entre la afirmación y el hecho. Cuando tu /// dice assert_eq!(duplica(21), 42), no está describiendo un comportamiento: lo está ejecutando, y si la realidad discrepa, la compilación se detiene. Esto reordena el papel del ejemplo en la enseñanza de una API. Un ejemplo escrito para el lector es, de por sí, valioso; pero un ejemplo que además se compila deja de ser una ilustración plausible para convertirse en una especificación viva, un fragmento del contrato que el sistema de construcción hace cumplir. La técnica de las líneas ocultas revela hasta qué punto se ha pensado el diseño: resuelve la vieja tensión entre el ejemplo pedagógico —que quiere ser breve— y el ejemplo correcto —que quiere ser completo— sin sacrificar ninguno de los dos, mostrando lo esencial y ejecutando el todo. Y compile_fail da el salto final: permite documentar no solo lo que tu código hace, sino lo que el sistema de tipos impide, atando incluso tus garantías negativas a una prueba. El efecto acumulado es una cultura donde la documentación no es el residuo que se escribe al final y se abandona, sino un artefacto de primera clase que se rompe con el código y se repara con él. En Rust, escribir un buen ejemplo no es un acto de generosidad opcional hacia el lector: es escribir una prueba más, y el compilador no distingue entre ambas cosas. Documentar es testear.

⚔️ Haz que tu documentación se gane la verdad
  1. Documenta una función pub con un ejemplo en /// que use assert_eq!; ejecuta cargo test --doc y confirma que aparece como prueba.
  2. Rompe a propósito la firma de esa función y observa cómo el doc test deja de compilar: has convertido la documentación en una guardiana.
  3. Escribe un ejemplo que use ? y añade la línea oculta # Ok::<(), E>(()); comprueba que el usuario no la ve pero el compilador sí.
  4. Documenta un uso que el borrow checker rechaza con un bloque compile_fail, y verifica que la prueba pasa porque no compila; luego rómpelo haciéndolo compilar.
  5. Marca con no_run un ejemplo que abriría un fichero o una conexión, y razona por qué no_run es más honesto que ignore para ese caso.