¿Qué pasaría si te dijera que puedes crear automáticamente documentación a partir de tus pruebas existentes que siempre estará actualizada?

¿Y si pudiera estar en formato Markdown, por lo que se distribuiría junto con el resto de tu código y se mostraría en GitLab / GitHub?

Suena muy bien, ¿verdad? Veamos cómo se hace.

Contexto

Personas como Simon Brown hacen un gran trabajo al convencerme de que no tengo suficiente documentación para mis proyectos. Y que la documentación debe estar actualizada y mostrar información concisa en una variedad de niveles de abstracción.

Me encantaría trabajar en un base de código con documentación como esa.

El Problema con la Documentación

He leído una buena cantidad de libros y artículos sobre arquitectura de software y cosas relacionadas. Pero nunca he podido reunir suficiente energía, o suficiente capital político, para poder crear documentación de acuerdo con este estándar. Y mucho menos mantenerlo actualizado.

Entonces, al menos para mi situación, necesito una forma de crear y actualizar documentación automáticamente.

También me gustaría almacenar los diagramas "como código", para que puedan registrarse en el repositorio. De esta manera, los cambios en ellos se pueden ver y discutir fácilmente en solicitudes de extracción y otras revisiones de código.

Hay muchas herramientas que pueden generar diagramas de dependencia del tiempo de construcción a partir del código, y he usado bastantes de ellas.

Pero el problema parece ser que estos diagramas siempre parecen espaguetis, incluso cuando el código es bueno. Y son complejos de configurar.

Parece muy difícil conseguir el nivel de detalle correcto. No hay forma de mostrar código relacionado en grupos lógicos para diagramas de alto nivel. Tampoco hay forma de seleccionar relaciones de código que sean específicas de un contexto particular para diagramas de bajo nivel.

Tampoco le brindan información sobre las relaciones de tiempo de ejecución del código, que generalmente es un problema mayor que las relaciones de tiempo de diseño.

Una solución

Para capturar las relaciones en tiempo de ejecución, la única opción es generar diagramas a partir del código en ejecución. Y ya tenemos mucho código que se ejecuta regularmente, en forma de pruebas.

Los repositorios ya deberían tener un buen conjunto de pruebas (unidad, integración y de extremo a extremo, por ejemplo), y cada prueba debería ser relativamente corta y sencilla.

Estas pruebas ya deberían incorporar agrupaciones lógicas de código y niveles sensibles de abstracción. Por lo que son un excelente candidato para generar documentación.

La solución implica instrumentar el código importado por un prueba. Este código instrumentado mantiene un registro de la jerarquía de llamadas en tiempo de ejecución y es capaz de escribir los resultados como un diagrama Mermaid en markdown (técnicamente un diagrama de secuencia).

Para cada prueba existente, crea una prueba de "envoltura", que es responsable de inicializar la jerarquía de llamadas y guardar el diagrama. Si tiene muchas pruebas, es posible que desees introducir un decorador para evitar la repetición.

from docs_from_tests.instrument_call_hierarchy import instrument_and_import_package, instrument_and_import_module, initialise_call_hierarchy, finalise_call_hierarchy
from samples.hello_world_combiner import HelloWorldCombiner

# puedes instrumentar paquetes / carpetas completos a la vez de esta manera
instrument_and_import_package(os.path.join(Path(__file__).parent.absolute(), '..', 'samples'), 'samples')
# Puedes instrumentar módulos individuales como este
# instrument_and_import_module('tests.blah')

# este es un contenedor alrededor de la prueba que también genera el diagrama de documentación / secuencia
def test_hello_world():
    # la inicializa la grabación de la jerarquía de llamadas
    initialise_call_hierarchy('start')

    # Esto ejecuta la prueba real
    _test_hello_world()
    
    # esto finaliza la jerarquía de llamadas y devuelve la raíz
    root_call = finalise_call_hierarchy()

    # esto devuelve un diagrama de secuencia de la jerarquía de llamadas
    sequence_diagram = root_call.sequence_diagram(
        show_private_functions=False,
        excluded_functions=[
            'HelloWorldCombiner.__init__',
        ]
    )

    # esto escribe la rebaja en el disco    
    sequence_diagram_filename = os.path.join(os.path.dirname(__file__), '..', 'doc', 'top-level-sequence-diagram.md')
    Path(sequence_diagram_filename).write_text(sequence_diagram)

# esta es la prueba original / fuente
def _test_hello_world():
    assert HelloWorldCombiner().combine() == 'Hello world'

Ejecutar pytest en este código dará como resultado la ejecución de la prueba y la creación del "diagrama como código" en markdown (a continuación) en el directorio doc:

sequenceDiagram
  start->>HelloWorldCombiner.combine: calls x1
  HelloWorldCombiner.combine->>hello: calls x1
  hello-->>HelloWorldCombiner.combine: returns str
  HelloWorldCombiner.combine->>world: calls x1
  world-->>HelloWorldCombiner.combine: returns str
  HelloWorldCombiner.combine-->>start: returns str

Esto representa como el siguiente diagrama:

Los cambios en el diagrama aparecerán en Git y se distribuirán junto con el código que provocó el cambio. Esto significa que el cambio en el código y el cambio en el diagrama están vinculados y se pueden ver juntos.

Los métodos privados generalmente se excluyen (aunque es opcional), y puede excluir otras funciones para que el gráfico tenga el aspecto deseado.

Debido a que la jerarquía de llamadas se almacena en una estructura de árbol, excluir una función también excluye todas las funciones incluidas en ella.

Calidad del código

Es de esperar que ya tengas pruebas en los niveles apropiados de abstracción (normalmente tendrías prueba unitarios, de integración y de extremo a extremo). Esto facilita la creación de diagramas en estos niveles.

Si no, entonces el deseo de crear buenos diagramas debería guiarlo hacia la creación también de buenas pruebas.

A veces, los diagramas se verán un poco desordenados y es posible que termines ignorando muchas funciones. Esta es una pista de que el código probablemente podría simplificarse. Y en este caso, el deseo de crear buenos diagramas debería guiarte hacia la simplificación del código.

Conclusión

¡Esperamos que este artículo le sirva como inspiración para crear y mantener la documentación que tus compañeros de equipo y tu futuro yo te agradecerán! Es bastante fácil de hacer.

Toda la funcionalidad está en un paquete de Python (docs-from-tests), y hay un repositorio de ejemplo que demuestra cómo usarlo.

Traducido del artículo de Cedd Burge - How to Create Documentation from Your Python Tests