Icono del sitio Profile Software Services

Documenta tus APIs con Spring REST Docs

A lo largo de mi experiencia en el desarrollo de software, he aprendido que una las principales cosas a tener en cuenta es cómo se va a hacer test del código que desarrollamos y no solo esto, también certificar que los test funcionan de la forma que debería y no crear test solo por cumplir un porcentaje de cobertura en nuestro código y así no tener problemas con las reglas de Sonar 😅. Una buena forma de documentar nuestro código es mediante test y, aunque existen otras alternativas para documentar nuestras APIs, cómo lo son swagger o asyncapi, en este caso me apoyaré en el proyecto Spring REST Docs. En este post veremos cómo generará un documento, en donde estará descrita toda la funcionalidad de nuestra API de una manera muy elegante e intuitiva.

¿Por qué trabajar con Spring Rest Docs?

En algún momento todos hemos implementado nuestros tests y con el pasar del tiempo hemos encontramos estrategias que nos ayudan con el desarrollo de éstos, pero también he sido testigo que cuando en un proyecto las fechas de entrega se empiezan a acortar se hacen dos principales cosas:

  1. Sacrificar los test (Decisión que se paga más adelante).
  2. Agregar desmedidamente desarrolladores al proyecto (Gran error si hablamos de plazos cortos).

Aunque no lo parezca, los test son un excelente medio al momento de documentar nuestro código. Pero para esto necesitamos tiempo para hacerlos y un equipo con un conocimiento mínimo en el proyecto, ya que si no es así solo desarrollaremos test que únicamente cumplirán con un porcentaje de cobertura. (En mi opinión) 😅

¿Cómo comenzar a usar Spring REST Docs?

Prerrequisitos:

Servicio Usuarios

Para crear nuestro proyecto de Spring Boot, vamos a https://start.spring.io/ y seleccionamos la siguiente configuración:

Agregamos el plugin de asciidoctor

Inicialmente, deberemos configurar el plugin de asciidoctor, el cual será el encargado de recopilar todos los documentos generados por nuestros tests(snippets). Las principales propiedades a configurar son: sourceDirectory, snippets y backend.

<plugin>
   <groupId>org.asciidoctor</groupId>
   <artifactId>asciidoctor-maven-plugin</artifactId>
   <version>${asciidoctor-maven-plugin.version}</version>
   <executions>
       <execution>
           <id>generate-docs</id>
           <phase>prepare-package</phase>
           <goals>
               <goal>process-asciidoc</goal>
           </goals>
           <configuration>
               <backend>spring-html</backend>
               <attributes>
                   <snippets>${snippetsDirectory}</snippets>
                   <project-version>${project.version}</project-version>
               </attributes>
               <sourceDirectory>src/docs/asciidoc</sourceDirectory>
           </configuration>
       </execution>
   </executions>
   <dependencies>
       <dependency>
           <groupId>io.spring.asciidoctor.backends</groupId>
           <artifactId>spring-asciidoctor-backends</artifactId>
           <version>${spring-asciidoctor-backends.version}</version>
       </dependency>
   </dependencies>
</plugin>

Tip: He reemplazado la dependencia: spring-restdocs-asciidoctor por spring-asciidoctor-backends para poder generar un documento con un estilo propuesto por el equipo de Spring, para ver el repositorio puedes consultar aquí.

UserController

Para nuestro servicio crearemos algunos end-point en los que podremos realizar operaciones CRUD sobre un usuario y sus posibles cuentas. Para no hacer tan extenso el desarrollo del servicio he usado H2 como base de datos en memoria y Spring JPA para poder realizar todas las operaciones a base de datos de manera sencilla y así enfocarnos en lo realmente importante: los tests. Pero si le quieres dar un ojo al código, lo puedes ver aquí.

UserControllerTest

Como lo mencione al inicio de este post, los test son un excelente medio al momento de documentar nuestro código. Así que, para este caso, usaremos MockMvc para realizar peticiones de ejemplo a nuestra API y usaremos el patrón ObjectModer para representar nuestros datos de entrada y salida, los cuales serán el contrato que deberá cumplir nuestro API.

Recuperar usuario por identificación

En este test recuperaremos la información básica de un usuario mediante su identificador y validaremos mediante jsonPath que los campos que debería regresar estén presentes en la respuesta del servicio. Hasta aquí todo parece normal, pero sí nos fijamos en los métodos pathParameters y responseFields veremos que estos son los encargados de validar mediante Spring Rest Docs que los campos que regresa el servicio son los que realmente debería regresar.

@Test
    @DisplayName("Retrieve basic user information by identifier")
    void retrieveBasicUserInformationByIdentifier() throws Exception {
        this.mockMvc.perform(get("/users/{id}", 1)
                        .contentType(APPLICATION_JSON))
                .andDo(document("user-by-id",
                        pathParameters(UserDtoMother.buildGetUserIdentifierPathParameters()),
                       responseFields(UserDtoMother.buildGetUserResponseFields()))
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").isNotEmpty())
                .andExpect(jsonPath("$.lastname").isNotEmpty())
                .andExpect(jsonPath("$.age").isNotEmpty())
                .andExpect(jsonPath("$.email").isNotEmpty());
    }

Nota: El valor user-by-id será el nombre con el que se generarán nuestros snippets, así que intentar dar un nombre que describa la intención de lo que hace.

 public static ParameterDescriptor buildGetUserIdentifierPathParameters() {
    Attributes.Attribute attribute = new Attributes.Attribute("exampleValue", "1");
    return fields.withPathParameter("id").description("User identifier").attributes(attribute);
  }

 public static List<FieldDescriptor> buildGetUserResponseFields() {
    return Arrays.asList(
        buildField("id", NUMBER, "User identifier", TRUE, fields),
        buildField("name", STRING, "User name", TRUE, fields),
        buildField("lastname", STRING, "User lastname", TRUE, fields),
        buildField("age", NUMBER, "User age", TRUE, fields),
        buildField("email", STRING, "User email", TRUE, fields),
        buildField("accounts", ARRAY, "Related user accounts", FALSE, fields));
  }

    

Nota: El método buildField es un utilitario que he usado para no tener que repetir la misma configuración de los objetos FieldDescriptor que son los que maneja Spring Rest Docs. Si quieres ver qué hace este método, lo puedes ver acá.

Snippets Generados

Una vez realizamos la configuración anterior y ejecutamos nuestros test, encontraremos todos los snippets generados en la ruta /target/generated-snippets con los identificadores que indicamos.

Como podemos ver, tendremos nuestros snippets, los cuales describen la funcionalidad prestada y su contenido será lo que capturo de la request o response de nuestro servicio.

Custom rest docs templates

Si vemos la documentación de Spring, podemos observar que podemos crear documentación bastante completa. Pero en algunos momentos necesitamos agregar información adicional que no está soportada por los templates base de Spring REST Docs. Aunque no nos debemos preocupar, los chicos de Spring pensaron en todos nosotros y nos dan la posibilidad de crear nuestros propios templates y modificar las propiedades que creamos necesarias para que todo luzca como lo queremos ver.

Para esto solo tendremos que cumplir con dos cosas, las cuales son:

Custom rest docs fields

En algunas ocasiones necesitamos información adicional en los templates proporcionados por Spring, pero por fortuna, para estos casos podemos modificar los templates y agregar los campos que creamos necesarios realizando algunas modificaciones en nuestro código.

En este caso, agregaremos un nuevo campo a nuestro snippet request-fields, al que llamaremos exampleValue.

|===
|Path|Type|Optional|Description|Constraints|Example Value

{{#fields}}
|{{path}}
|{{type}}
|{{optional}}
|{{description}}
|{{constraints}}
|{{exampleValue}}

{{/fields}}
|===

Este nuevo campo lo deberemos de enviar al momento de construir nuestra clase FieldDescriptor, ya que spring necesita saber qué valor le deberá asignar a este campo.
private FieldDescriptor buildField(
            String name,
            JsonFieldType type,
            String description,
            boolean isMandatory,
            ConstrainedFields fields,
            String... exampleValues
    ) {
        String exampleValue = String.join(DELIMITER, exampleValues);
        Attributes.Attribute attribute = new Attributes.Attribute(EXAMPLE_KEY_VALUE, exampleValue);
        FieldDescriptor fieldDescriptor = fields
                .withPath(name)
                .type(type)
                .description(description)
                .attributes(attribute);

        if (!isMandatory) {
            fieldDescriptor.optional();
        }
        return fieldDescriptor;
    }

Asciidoc template

Este será nuestro template base, el cual será tenido en cuenta al momento de generar nuestro documento final. Este documento será de tipo .adoc, el cual estará basado en el procesador https://asciidoctor.org.

En este haremos referencia a todos nuestros snippets y agregaremos información que creamos relevante para la documentación de nuestra API.

Puedes ver un ejemplo aquí.

Agregando información adicional a nuestro template Asciidoc

En ocasiones necesitaremos poner información diferente a la de nuestros snippets de manera dinámica. Por fortuna, este tipo de cosas son posibles de manera muy simple. Para esto, iremos a nuestro pom y en el plugin asciidoctor-maven-plugin agregaremos toda la información que necesitamos dentro de la etiqueta “attributes”.

Acá podremos poner toda la información que creamos importante, como lo puede ser la versión del artefacto al generar la documentación o el identificador del commit con el que se generó. En fin, hay un sin fin de posibilidades.

Una vez agregara esta información a nuestro pom.xml, deberemos ir a nuestra plantilla index.adoc y hacer referencia al nombre de la etiqueta dentro de la expresión “{}”.

Al momento de ejecutar nuestros tests, Spring generará la documentación y tendrá en cuenta los snippets a los que hubiéramos hecho referencia, junto a los parámetros en nuestra plantilla. Simplemente usando la expresión “{}”, Spring entenderá que son parámetros de entrada y asignará el valor que este tenga definido en nuestro pom.xml.

Publicando nuestra documentación con GitHub actions

Hasta ahora hemos visto cómo generar nuestra documentación de una forma bastante automatizada y descriptiva. Pero a pesar de que nuestra documentación es bastante completa, tenemos un problema y es que todo está alojado en la carpeta target de nuestro proyecto y esta es una mala ubicación si nuestro objetivo es tener la documentación de nuestra API disponible para ser consumida en cualquier momento.

Para esto, usaremos GitHub Actions, la cual es una herramienta bastante útil que nos ayudará a realizar procesos de CI/CD en nuestros proyectos. En este caso en concreto, haremos uso de esta funcionalidad para generar la documentación mediante la ejecución de los test y, con la documentación que generará, publicarla en GitHub Pages para poder tener un sitio centralizado en donde poder consultar la documentación de nuestra API.

Configuration File

Para poder hacer uso de esta funcionalidad, deberemos crear un fichero con extensión .yml, el cual deberá estar en el directorio .github/workflows de la raíz de nuestro repositorio. Este fichero aloja toda la configuración que creamos necesaria para poder ejecutar nuestro proceso.

Aunque la documentación es mucho más completa y precisa de lo que yo pueda ser, veamos un poco que tiene este fichero.

En el tag denominado como “name” indicaremos el nombre de nuestro workflow. Seguidamente, en el tag “on” indicaremos las ramas a tener en cuenta al momento de hacer push o un pull request. En el tag “steps”, definiremos todos los pasos que ejecutara nuestra acción al momento de ejecutarse. Por último indicaremos en el tag “publish_dir” la ruta de los ficheros generados, los cuales están en la ruta: target/generated-docs.

name: Build
 
on:
 push:
   branches:
     - '**'
 pull_request:
   branches: [ master ]
 
jobs:
 build:
   name: "Build API documentation"
   strategy:
     matrix:
       java: [ 11 ]
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v2
 
     - name: Setup java ${{ matrix.java }}
       uses: actions/setup-java@v1
       with:
         java-version: ${{ matrix.java }}
 
     - name: maven-settings-xml-action
       uses: whelk-io/maven-settings-xml-action@v20
       with:
         plugin_repositories: >
           [
             {
               "id": "spring.release",
               "name": "spring.release",
               "url": "https://repo.spring.io/plugins-release"
             }
           ]
         path: ~/.m2/repository
         key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
         restore-keys: |
           ${{ runner.os }}-maven-
     - name: Build with Maven
       run: ./mvnw clean verify
     - name: GitHub Pages action
       if: |
         github.ref == 'refs/heads/master' &&
          matrix.java == 11
       uses: peaceiris/actions-gh-pages@v3.7.3
       with:
         github_token: ${{ secrets.GITHUB_TOKEN }}
         publish_dir: ./target/generated-docs

La etiqueta plugin_repositories es necesaria, ya que en el plugin asciidoctor-maven-plugin del pom.xml he cambiado la dependencia spring-restdocs-asciidoctor por spring-asciidoctor-backends, para poder tener un estilo de documento diferente. Pero si no has realizado este cambio, esta configuración no es necesaria. Algo a tener en cuenta es que nuestro template se deberá llamar index.adoc, ya que si lo nombramos de otra manera GitHub no reconocerá el fichero y no cargará nuestro sitio web. (Lo digo porque me pasó 😅)

Si hacemos push o un pull request a master, nuestra acción se ejecutara y veremos algo como esto:

Si todo ha salido bien, deberíamos ver en la url de GitHub Pages toda la documentación de nuestra API: https://dberna2.github.io/spring-rest-docs-gh-pages/

Pueden ver el ejemplo completo en: https://github.com/dberna2/spring-rest-docs-gh-pages

Hay varias cosas que seguro me dejé en el aire, así que siempre serán bienvenidos comentarios de mejora tanto en el post, como en el código 😄💪. ¡Compártelo en nuestras redes sociales!

Salir de la versión móvil