Testes Unitários de API REST: Configuração Standalone

Ao concluirmos esta aula, nós:

  • Saberemos como declarar e obter as dependências necessárias para testar APIs REST com Maven e Gradle 9 (Kotlin DSL).
  • Seremos capazes de identificar a configuração mínima necessária quando escrevemos testes unitários para uma API REST em modo standalone.
  • Compreenderemos como configurar o sistema sob teste (SUT) para utilizar nosso próprio ObjectMapper customizado (com KotlinModule e suporte a java.time).

Obtendo as Dependências Necessárias

Quando queremos escrever testes unitários para uma API REST que consome e produz documentos JSON usando o Spring MVC Test framework, precisamos adicionar as seguintes dependências na nossa configuração de build:

  • Jackson Databind e Jackson Module Kotlin (com.fasterxml.jackson.core:jackson-databind e com.fasterxml.jackson.module:jackson-module-kotlin): O Spring MVC utiliza o Jackson para serializar objetos de retorno em documentos JSON e desserializar o corpo das requisições HTTP (@RequestBody) em data classes Kotlin.
  • Jayway JsonPath (com.jayway.jsonpath:json-path): O Spring MVC Test framework utiliza o JsonPath por baixo dos panos quando escrevemos asserções para a estrutura e os valores de documentos JSON retornados usando jsonPath(...).
  • JUnit Jupiter 6 (org.junit.jupiter:junit-jupiter): Nosso framework de execução de testes.
  • MockK (io.mockk:mockk): Nossa biblioteca idiomática em Kotlin para substituir os serviços chamados pelo @RestController por dublês de teste (stubs e mocks).
  • Spring Test e Spring Web MVC (org.springframework:spring-test e org.springframework:spring-webmvc, ou simplesmente org.springframework.boot:spring-boot-starter-test e org.springframework.boot:spring-boot-starter-web).
  • Jakarta Servlet API (jakarta.servlet:jakarta.servlet-api): Necessária para compilar e executar o MockHttpServletRequest e MockHttpServletResponse em memória.

Dica Prática com Spring Boot 4.1: Ao adicionar testImplementation("org.springframework.boot:spring-boot-starter-test") e implementation("org.springframework.boot:spring-boot-starter-web") no seu build.gradle.kts, todas as dependências acima (exceto o MockK, que adicionamos explicitamente) já são incluídas e gerenciadas automaticamente na versão compatível!

Caso você esteja configurando um projeto com gerenciamento manual de dependências no Maven (pom.xml), as dependências necessárias são:

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<dependencies>
    <!-- Jackson + Suporte a Data Classes Kotlin -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.18.2</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.module</groupId>
        <artifactId>jackson-module-kotlin</artifactId>
        <version>2.18.2</version>
    </dependency>

    <!-- JsonPath para asserções JSON no MockMvc -->
    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <version>2.9.0</version>
        <scope>test</scope>
    </dependency>

    <!-- JUnit Jupiter 6 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>6.0.3</version>
        <scope>test</scope>
    </dependency>

    <!-- MockK -->
    <dependency>
        <groupId>io.mockk</groupId>
        <artifactId>mockk-jvm</artifactId>
        <version>1.14.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Jakarta Servlet API -->
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.1.0</version>
        <scope>provided</scope>
    </dependency>

    <!-- Spring Framework (WebMVC + Test) -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>7.0.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>7.0.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Se estivermos usando o Gradle 9 (Kotlin DSL — build.gradle.kts), nossa configuração fica assim:

1
2
3
4
5
6
7
8
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("com.jayway.jsonpath:json-path")
    testImplementation("io.mockk:mockk:1.14.0")
}

Vamos agora descobrir como configurar o sistema sob teste no modo standalone!


Configurando o Sistema Sob Teste (SUT)

A configuração standalone que devemos usar ao escrever testes unitários para uma API REST depende da complexidade da nossa aplicação e de como serializamos objetos para JSON. Vamos examinar dois cenários comuns:

  1. Nossa aplicação utiliza a configuração padrão do Spring MVC (não customiza o ObjectMapper nem os HttpMessageConverters).
  2. Nossa aplicação utiliza um ObjectMapper customizado.

Caso 1: Usando a Configuração Padrão

Se a nossa API REST apenas retorna objetos simples (ou data classes) e não exige configurações especiais de serialização no ObjectMapper, precisamos apenas instanciar o controlador @RestController passando seus dublês de teste (mockk()) via construtor e construir o MockMvc com MockMvcBuilders.standaloneSetup(...).

Suponha que estejamos testando a classe TaskCrudController:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.testwithspring.starter.web

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

@RestController
@RequestMapping("/api/task")
class TaskCrudController(
    private val crudService: TaskCrudService
) {
    // Métodos dos endpoints REST
}

A configuração mínima da nossa classe de teste (TaskCrudControllerTest) é extraordinariamente simples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.testwithspring.starter.web

import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.MockMvcBuilders

class TaskCrudControllerTest {

    private val crudService: TaskCrudService = mockk()
    private lateinit var mockMvc: MockMvc

    @BeforeEach
    fun configureSystemUnderTest() {
        mockMvc = MockMvcBuilders.standaloneSetup(TaskCrudController(crudService))
            .build()
    }
}

Quando o MockMvcBuilders.standaloneSetup(...) é executado e detecta o Jackson no classpath, ele registra automaticamente uma instância padrão de MappingJackson2HttpMessageConverter.

Contudo, em aplicações reais com Spring Boot e Kotlin, é muito comum customizarmos o ObjectMapper (por exemplo, para formatar datas java.time.Instant / ZonedDateTime em ISO-8601 em vez de timestamps numéricos, ignorar campos nulos ou registrar o KotlinModule). Como garantimos que o teste unitário standalone use exatamente o mesmo ObjectMapper da produção?

Caso 2: Usando um ObjectMapper Customizado

Suponha que nossa aplicação possua um método factory (ou @Bean) que configura um ObjectMapper customizado:

  • Ele desabilita a feature SerializationFeature.WRITE_DATES_AS_TIMESTAMPS para que datas e horas sejam escritas como strings no padrão ISO-8601 (ex: "2026-11-01T12:00:00Z").
  • Ele registra o JavaTimeModule (para dar suporte à API de data e hora do Java 8+) e o KotlinModule (para suporte completo a construtores primários de data classes Kotlin).

Podemos criar uma função factory estática na nossa classe WebTestConfig que instancia esse ObjectMapper e o envolve em um MappingJackson2HttpMessageConverter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.testwithspring.starter.web

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter

object WebTestConfig {

    fun objectMapper(): ObjectMapper {
        return ObjectMapper().apply {
            disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            registerModule(JavaTimeModule())
            registerKotlinModule()
        }
    }

    fun objectMapperHttpMessageConverter(): MappingJackson2HttpMessageConverter {
        return MappingJackson2HttpMessageConverter().apply {
            objectMapper = objectMapper()
        }
    }
}

Agora, no método @BeforeEach da nossa classe de teste (TaskCrudControllerTest), podemos instruir o StandaloneMockMvcBuilder a usar o nosso MappingJackson2HttpMessageConverter customizado chamando o método .setMessageConverters(...):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.testwithspring.starter.web

import com.testwithspring.starter.web.WebTestConfig.objectMapperHttpMessageConverter
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.MockMvcBuilders

class TaskCrudControllerTest {

    private val crudService: TaskCrudService = mockk()
    private lateinit var mockMvc: MockMvc

    @BeforeEach
    fun configureSystemUnderTest() {
        mockMvc = MockMvcBuilders.standaloneSetup(TaskCrudController(crudService))
            .setMessageConverters(objectMapperHttpMessageConverter())
            .build()
    }
}

Atenção ao usar .setMessageConverters(...): Quando invocamos .setMessageConverters(...) no StandaloneMockMvcBuilder, ele substitui a lista padrão de conversores HTTP pelos conversores que você passar. Na grande maioria das APIs REST JSON, o MappingJackson2HttpMessageConverter é tudo o que você precisa!


Resumo

Esta aula nos ensinou três coisas fundamentais:

  • O Spring MVC Test framework utiliza o Jayway JsonPath (com.jayway.jsonpath:json-path) quando escrevemos asserções sobre documentos JSON retornados pela API.
  • Na maioria dos casos simples, não precisamos adicionar nenhuma configuração extra ao StandaloneMockMvcBuilder além de passar o controlador REST sob teste para MockMvcBuilders.standaloneSetup(...).
  • Se a nossa aplicação utiliza um ObjectMapper customizado (como formatação ISO-8601 para datas java.time e KotlinModule), devemos criar um MappingJackson2HttpMessageConverter que utilize esse ObjectMapper e registrá-lo com .setMessageConverters(...) ao construir o MockMvc.