Especificando Controladores Spring MVC com Kotest: Configuração

Depois de concluirmos esta aula, compreenderemos como configurar o Spring MVC Test Framework (MockMvc em modo standalone) quando estivermos escrevendo testes unitários com o Kotest Framework 6 e MockK.


1. Configurando o Sistema Sob Especificação

Quando escrevemos testes unitários para controladores Spring MVC usando o Kotest, configuramos o sistema sob especificação criando uma instância de MockMvc que será utilizada pelos blocos de teste da nossa especificação.

Para garantir que nossos testes unitários sejam ultrarrápidos (executando em poucos milissegundos sem subir o ApplicationContext do Spring) e 100% isolados entre si, seguimos três passos:

  1. Identificamos as dependências exigidas pelo construtor do controlador testado.
  2. Criamos os dublês de teste necessários (mockk<TaskCrudService>()) e os armazenamos em propriedades isoladas por caso de teste (com IsolationMode.InstancePerLeaf ou resetando-os no beforeEach). Isso garante que cada caso de teste receba seus próprios dublês limpos e que a configuração feita em um teste nunca afete os demais.
  3. Criamos uma instância de MockMvc usando a configuração standalone (MockMvcBuilders.standaloneSetup(...)) e a armazenamos em uma propriedade isolada da especificação. Assim, cada caso de teste interage com seu próprio controlador conectado a dublês limpos.

Vamos ver como aplicar esses três passos primeiro para um controlador web tradicional (@Controller) e depois para um controlador de API REST (@RestController).


2. Configurando Testes Unitários para um Controlador Web Tradicional (@Controller)

Suponha que estejamos escrevendo especificações unitárias para a classe TaskCrudController (versão web tradicional baseada em views):

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

import com.testwithspring.master.task.TaskCrudService
import org.springframework.context.MessageSource
import org.springframework.stereotype.Controller

@Controller
class TaskCrudController(
    private val crudService: TaskCrudService,
    private val messageSource: MessageSource
) {
    // Métodos de handler omitidos
}

Como podemos ver, esse controlador possui duas dependências no construtor:

  • TaskCrudService: fornece as operações de CRUD para tarefas.
  • MessageSource: usado para buscar mensagens internacionalizadas (como mensagens flash de feedback exibidas ao usuário após criar ou excluir uma tarefa).

Depois de identificarmos as dependências do controlador, configuramos o sistema sob especificação na nossa classe TaskCrudControllerSpec seguindo três passos:

  1. Criamos um novo objeto StaticMessageSource e o armazenamos em uma propriedade isolada. Usamos StaticMessageSource do próprio Spring porque ele é um fake em memória que nos permite registrar mensagens programaticamente (messageSource.addMessage(...)), o que é muito mais limpo do que fazer stubbing de métodos do MessageSource.
  2. Criamos um mock para TaskCrudService com mockk<TaskCrudService>().
  3. Criamos o objeto MockMvc usando MockMvcBuilders.standaloneSetup(...) auxiliado pelos métodos factory da nossa classe Object Mother WebTestConfig (exceptionResolver(), fixedLocaleResolver() e jspViewResolver()):
 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.web

import com.testwithspring.master.UnitTest
import com.testwithspring.master.task.TaskCrudService
import com.testwithspring.master.web.WebTestConfig.exceptionResolver
import com.testwithspring.master.web.WebTestConfig.fixedLocaleResolver
import com.testwithspring.master.web.WebTestConfig.jspViewResolver
import io.kotest.core.annotation.Tags
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.mockk.mockk
import org.springframework.context.support.StaticMessageSource
import org.springframework.test.web.servlet.setup.MockMvcBuilders

@Tags("UnitTest")
class TaskCrudControllerSpec : BehaviorSpec({

    isolationMode = IsolationMode.InstancePerLeaf

    val messageSource = StaticMessageSource()
    val service = mockk<TaskCrudService>()
    val mockMvc = MockMvcBuilders
        .standaloneSetup(TaskCrudController(service, messageSource))
        .setHandlerExceptionResolvers(exceptionResolver())
        .setLocaleResolver(fixedLocaleResolver())
        .setViewResolvers(jspViewResolver())
        .build()

    // Os blocos Given / When / Then virão nas próximas duas aulas!
})

[!NOTE] A classe utilitária WebTestConfig é exatamente a mesma que construímos no Tópico 07 do Capítulo 01 (Pacote Iniciante), fornecendo os métodos estáticos exceptionResolver(), fixedLocaleResolver(), jspViewResolver() e objectMapperHttpMessageConverter() para evitar duplicação na configuração do MockMvcBuilders.standaloneSetup(...).

Vamos ver agora como configurar o sistema sob especificação quando estamos testando uma API REST!


3. Configurando Testes Unitários para uma API REST (@RestController)

Agora suponha que estejamos escrevendo especificações unitárias para a versão REST de TaskCrudController, que processa requisições enviadas para /api/task:

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

import com.testwithspring.master.task.TaskCrudService
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/task")
class TaskCrudRestController(
    private val crudService: TaskCrudService
) {
    // Endpoints REST omitidos
}

Como podemos ver, este controlador possui apenas uma dependência: TaskCrudService.

Para configurar o sistema sob especificação na nossa classe TaskCrudRestControllerSpec, seguimos dois passos:

  1. Criamos um mock para TaskCrudService usando mockk<TaskCrudService>() em uma propriedade isolada.
  2. Criamos o objeto MockMvc usando MockMvcBuilders.standaloneSetup(...), registrando o @ControllerAdvice de tratamento de erros (TaskErrorHandler()), o fixedLocaleResolver() e o conversor HTTP JSON do Jackson (objectMapperHttpMessageConverter()):
 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
package com.testwithspring.master.web

import com.testwithspring.master.UnitTest
import com.testwithspring.master.task.TaskCrudService
import com.testwithspring.master.web.WebTestConfig.fixedLocaleResolver
import com.testwithspring.master.web.WebTestConfig.objectMapperHttpMessageConverter
import io.kotest.core.annotation.Tags
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.BehaviorSpec
import io.mockk.mockk
import org.springframework.test.web.servlet.setup.MockMvcBuilders

@Tags("UnitTest")
class TaskCrudRestControllerSpec : BehaviorSpec({

    isolationMode = IsolationMode.InstancePerLeaf

    val service = mockk<TaskCrudService>()
    val mockMvc = MockMvcBuilders
        .standaloneSetup(TaskCrudRestController(service))
        .setControllerAdvice(TaskErrorHandler())
        .setLocaleResolver(fixedLocaleResolver())
        .setMessageConverters(objectMapperHttpMessageConverter())
        .build()

    // Os blocos Given / When / Then para leitura e escrita JSON virão nas Aulas 05 e 06!
})

Resumo

Nesta aula, aprendemos dois princípios fundamentais para configurar testes unitários de controladores Spring MVC no Kotest:

  • Devemos armazenar as dependências do controlador testado (service, messageSource) em propriedades isoladas por caso de teste (IsolationMode.InstancePerLeaf) para garantir que o stubbing ou as interações de um cenário nunca afetem outros cenários.
  • Devemos instanciar o MockMvc via MockMvcBuilders.standaloneSetup(...) também em uma propriedade isolada por teste, garantindo que cada cenário execute contra um controlador limpo e sem custo de inicialização do contexto Spring.