Documentando APIs com Spring REST Docs: Configuração

Depois de concluirmos este webinar, nós:

  • Saberemos configurar um projeto (em Gradle 9 ou Maven) que gera a documentação da nossa API automaticamente durante o build.
  • Compreenderemos o que é o Spring REST Docs.
  • Saberemos configurar o Spring REST Docs quando estivermos usando o Spring Boot 4 e o JUnit 6 (tanto em testes unitários standalone quanto em testes de integração).
  • Compreenderemos como podemos documentar nossa API REST com Spring REST Docs e Asciidoctor.

Este webinar assume que:

  • Você sabe especificar o contexto de aplicação carregado pelo Spring TestContext Framework.
  • Você está familiarizado com o JUnit 6.
  • Você sabe criar um projeto que executa testes unitários, de integração e End-to-End com JUnit 6.

Uma Rápida Introdução ao Spring REST Docs

O Spring REST Docs é uma biblioteca que nos permite documentar uma API REST escrevendo testes automatizados para ela. O Spring REST Docs utiliza Asciidoctor por padrão (embora também seja possível usar Markdown).

Quando queremos documentar uma API REST com o Spring REST Docs, temos que seguir estes dois passos:

  1. Gerar os snippets que documentam o comportamento da nossa API REST: Para ser mais específico, esses snippets descrevem tanto as requisições HTTP consumidas pelos nossos recursos de API quanto as respostas HTTP retornadas por eles. Podemos gerar esses snippets utilizando o Spring MVC Test framework (MockMvc), o WebTestClient do Spring WebFlux ou o REST Assured.
  2. Escrever o documento Asciidoctor (index.adoc) que especifica a estrutura da documentação da nossa API: Quando queremos documentar nossos recursos de API, incluímos (include::...) os snippets gerados automaticamente pelo Spring REST Docs.

Agora que conhecemos o funcionamento do Spring REST Docs, vamos descobrir como declarar as dependências e plugins necessários no nosso build.

Obtendo as Dependências e Configurando o Asciidoctor no Build

Como queremos integrar o Spring REST Docs com o Spring MVC Test framework (MockMvc), temos que declarar a dependência spring-restdocs-mockmvc no escopo de teste e configurar o plugin do Asciidoctor.

Opção A: Configuração no Gradle 9 (build.gradle.kts)

No Gradle 9 com Kotlin DSL, aplicamos o plugin org.asciidoctor.jvm.convert, declaramos a configuração asciidoctorExt para o spring-restdocs-asciidoctor e configuramos o diretório de saída dos snippets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
plugins {
    kotlin("jvm") version "2.4.21"
    kotlin("plugin.spring") version "2.4.21"
    id("org.springframework.boot") version "4.1.2"
    id("io.spring.dependency-management") version "1.1.7"
    id("org.asciidoctor.jvm.convert") version "4.0.4"
}

val asciidoctorExt: Configuration by configurations.creating
val snippetsDir by extra { file("build/generated-snippets") }

dependencies {
    asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor")
    testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
}

tasks.test {
    outputs.dir(snippetsDir)
}

tasks.asciidoctor {
    inputs.dir(snippetsDir)
    configurations(asciidoctorExt.name)
    dependsOn(tasks.test)
    baseDirFollowsSourceFile()
    attributes(
        mapOf(
            "snippets" to snippetsDir,
            "doctype" to "book"
        )
    )
}

Opção B: Configuração Equivalente no Maven (pom.xml)

Em projetos Maven, declaramos a dependência spring-restdocs-mockmvc:

1
2
3
4
5
<dependency>
    <groupId>org.springframework.restdocs</groupId>
    <artifactId>spring-restdocs-mockmvc</artifactId>
    <scope>test</scope>
</dependency>

E configuramos o asciidoctor-maven-plugin para ser executado na fase prepare-package gerando um documento HTML com doctype igual a book e a dependência spring-restdocs-asciidoctor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<plugin>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctor-maven-plugin</artifactId>
    <version>3.1.0</version>
    <executions>
        <execution>
            <id>generate-docs</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>process-asciidoc</goal>
            </goals>
            <configuration>
                <backend>html</backend>
                <doctype>book</doctype>
            </configuration>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.springframework.restdocs</groupId>
            <artifactId>spring-restdocs-asciidoctor</artifactId>
            <version>${spring-restdocs.version}</version>
        </dependency>
    </dependencies>
</plugin>

Antes de vermos como usar o Spring REST Docs com o MockMvc, vamos dar uma rápida olhada no sistema sob teste.

Introdução ao Sistema sob Teste

Durante este webinar, escreveremos testes unitários e de integração para um método controlador simples que processa requisições GET enviadas ao caminho /api/hello. Este método simplesmente retorna a string "Hello World!" no corpo da resposta.

O código-fonte da classe HelloWorldController em Kotlin é o seguinte:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.testwithspring.master.hello

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/hello")
class HelloWorldController {

    @GetMapping
    fun sayHello(): String = "Hello World!"
}

Vamos avançar e descobrir como podemos usar o Spring REST Docs quando estamos escrevendo testes unitários para este controlador.

Gerando Snippets em Testes Unitários (Standalone Setup)

Quando estamos escrevendo testes unitários para um controlador Spring MVC, configuramos o sistema sob teste usando o modo standalone (MockMvcBuilders.standaloneSetup(...)). Para integrar o Spring REST Docs com o MockMvc no modo standalone e JUnit 6, seguimos estes passos:

  1. Integrar o Spring REST Docs com o JUnit 6 anotando a classe de teste com @ExtendWith(RestDocumentationExtension::class).
  2. Adicionar uma propriedade mockMvc à nossa classe de teste.
  3. Criar um método @BeforeEach que é executado antes de cada método de teste. Como estamos usando a RestDocumentationExtension, nosso método de setup recebe um parâmetro do tipo RestDocumentationContextProvider.
  4. Implementar o método @BeforeEach criando um novo objeto MockMvc e aplicando documentationConfiguration(restDocumentation).
  5. Criar uma classe interna @Nested (SayHello) e adicionar um método de teste que envia a requisição GET /api/hello e invoca .andDo(document("say-hello")) da classe MockMvcRestDocumentation, passando o identificador único do recurso documentado ("say-hello").

Após termos escrito o método de teste que documenta o comportamento do nosso recurso de API, o código-fonte da nossa classe de teste fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.testwithspring.master.hello

import com.testwithspring.master.TestTags
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.restdocs.RestDocumentationContextProvider
import org.springframework.restdocs.RestDocumentationExtension
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder

@ExtendWith(RestDocumentationExtension::class)
@Tag(TestTags.UNIT_TEST)
class HelloWorldControllerTest {

    private lateinit var mockMvc: MockMvc

    @BeforeEach
    fun configureSystemUnderTest(restDocumentation: RestDocumentationContextProvider) {
        this.mockMvc = MockMvcBuilders.standaloneSetup(HelloWorldController())
            .apply<StandaloneMockMvcBuilder>(documentationConfiguration(restDocumentation))
            .build()
    }

    @Nested
    @DisplayName("Say Hello")
    inner class SayHello {

        @Test
        @DisplayName("Cria os snippets do Spring REST Docs")
        fun createSpringRESTDocsSnippets() {
            mockMvc.perform(get("/api/hello"))
                .andDo(document("say-hello"))
        }
    }
}

Há dois pontos importantes que quero destacar:

  • Por padrão, o Spring REST Docs gera os snippets Asciidoc no diretório build/generated-snippets/<identificador-do-recurso> (no Gradle) ou target/generated-snippets/<identificador-do-recurso> (no Maven). O identificador do recurso é a string única passada ao método document("say-hello").
  • Nossa classe de teste naturalmente possui outros testes unitários (que verificam status HTTP 200 e corpo "Hello World!"). Contudo, é uma excelente prática escrever um método de teste dedicado exclusivamente a gerar os snippets do Spring REST Docs, garantindo que cada método de teste tenha uma única responsabilidade.

Agora sabemos gerar os snippets Asciidoctor ao escrever testes unitários. Vamos avançar e descobrir como podemos gerar esses snippets ao escrever testes de integração.

Gerando Snippets em Testes de Integração (@AutoConfigureRestDocs)

Quando estamos escrevendo testes de integração para um controlador Spring MVC no Spring Boot 4, queremos configurar o sistema sob teste usando o WebApplicationContext real. Podemos integrar o Spring REST Docs com o MockMvc de forma totalmente declarativa seguindo estes passos:

  1. Configurar a classe de aplicação Spring Boot com @SpringBootTest(classes = [ExampleApplication::class]).
  2. Garantir que o Spring Boot crie e configure um objeto MockMvc anotando a classe com @AutoConfigureMockMvc.
  3. Garantir que o Spring REST Docs seja autoconfigurado pelo Spring Boot anotando a classe com @AutoConfigureRestDocs.
  4. Injetar o bean MockMvc na nossa classe de teste e invocar .andDo(document("say-hello")).

Após termos escrito o teste de integração que documenta nosso recurso de API, o código-fonte da classe SayHelloTest fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.testwithspring.master.hello

import com.testwithspring.master.ExampleApplication
import com.testwithspring.master.TestTags
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get

@SpringBootTest(classes = [ExampleApplication::class])
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Tag(TestTags.INTEGRATION_TEST)
class SayHelloTest(
    @Autowired private val mockMvc: MockMvc
) {

    @Test
    @DisplayName("Cria os snippets do Spring REST Docs")
    fun createSpringRESTDocsSnippets() {
        mockMvc.perform(get("/api/hello"))
            .andDo(document("say-hello"))
    }
}

Agora sabemos gerar os snippets tanto em testes unitários quanto em testes de integração. A seguir, descobriremos como escrever o documento Asciidoctor da nossa API.

Escrevendo a Documentação da Nossa API com Asciidoctor

Quando escrevemos a documentação da API usando o Spring REST Docs, criamos um documento Asciidoctor (src/docs/asciidoc/index.adoc no Gradle ou src/main/asciidoc/index.adoc no Maven) e incluímos os snippets gerados dentro desse documento.

Primeiro, criamos a estrutura básica do documento com uma seção chamada Resources:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
= Hello World Example
Petri Kainulainen;
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:

[[resources]]
= Resources

Segundo, documentamos nosso recurso de API seguindo estes passos:

  1. Adicionar uma subseção chamada Say Hello à seção Resources.
  2. Descrever a requisição HTTP que é usada para obter uma mensagem do nosso recurso de API (incluindo o snippet curl-request.adoc).
  3. Descrever o conteúdo da resposta HTTP retornada (incluindo o snippet http-response.adoc).

Após termos documentado nosso recurso de API, o conteúdo do nosso arquivo Asciidoctor fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
= Hello World Example
Petri Kainulainen;
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:

[[resources]]
= Resources

[[resources-say-hello]]
== Say Hello

Podemos obter uma mensagem da nossa API REST enviando uma requisição `GET` para
o caminho: `/api/hello`. O exemplo a seguir demonstra como podemos enviar
esta requisição HTTP usando curl:

include::{snippets}/say-hello/curl-request.adoc[]

Quando enviamos esta requisição `GET`, nossa API REST retorna o status HTTP 200 e
a mensagem: 'Hello World!' de volta para nós. O snippet a seguir descreve o
conteúdo da resposta HTTP retornada:

include::{snippets}/say-hello/http-response.adoc[]

Nota: O atributo {snippets} é configurado automaticamente pelo spring-restdocs-asciidoctor e aponta para o diretório raiz onde os snippets foram gerados (build/generated-snippets ou target/generated-snippets).

Gerando a Documentação HTML da Nossa API

Podemos gerar a documentação final da nossa API executando:

1
2
3
4
5
## Com Gradle 9:
./gradlew clean asciidoctor
## Com Maven:
mvn clean package -P dev
mvn clean package -P integration-test

Quando executamos esse comando, o plugin do Asciidoctor processa o index.adoc junto com os snippets gerados pelos testes e cria a documentação HTML pronta em build/docs/asciidoc (no Gradle) ou target/generated-docs (no Maven).

Vamos resumir o que aprendemos neste webinar.

Resumo

Este webinar nos ensinou sete coisas:

  • O Spring REST Docs gera snippets que documentam com precisão o comportamento real do recurso de API testado.
  • Podemos gerar esses snippets usando o Spring MVC Test framework (MockMvc), WebTestClient ou REST Assured.
  • Antes de usar o Spring REST Docs com MockMvc, devemos declarar a dependência spring-restdocs-mockmvc no escopo de teste.
  • Geramos a documentação HTML final usando o plugin do Asciidoctor (com a extensão spring-restdocs-asciidoctor).
  • Ao documentar nossa API REST em testes unitários standalone, usamos @ExtendWith(RestDocumentationExtension::class) e documentationConfiguration(restDocumentation).
  • Ao documentar nossa API REST em testes de integração Spring Boot, basta anotar a classe de teste com @AutoConfigureMockMvc e @AutoConfigureRestDocs.
  • Geramos os snippets de um recurso invocando .andDo(document("<identificador-do-recurso>")) da classe MockMvcRestDocumentation.