Testes Unitários de Controladores Web: Configuração

Ao concluirmos esta aula, nós:

  • Saberemos como selecionar criteriosamente apenas os componentes necessários para testar um controlador web.
  • Compreenderemos como criar e incluir componentes adicionais (SimpleMappingExceptionResolver, FixedLocaleResolver, ViewResolver e StaticMessageSource) no Sistema Sob Teste usando WebTestConfig e StandaloneMockMvcBuilder.

[!NOTE] Esta aula assume que:


De Quais Componentes Nós Realmente Precisamos?

Quando utilizamos a configuração standalone, podemos selecionar quais componentes queremos incluir no Sistema Sob Teste através da API da classe StandaloneMockMvcBuilder. Essa grande flexibilidade é uma faca de dois gumes e pode causar dois problemas se não tomarmos cuidado:

  1. Primeiro, configuração enganosa: Se incluirmos um componente extra no Sistema Sob Teste, qualquer pessoa que leia nosso código de teste assumirá que aquele componente é exigido pelos casos de teste daquela classe. Se ele não for necessário ali, nossa configuração se torna enganosa e dificulta a leitura (fazendo o desenvolvedor perder tempo precioso investigando componentes irrelevantes quando um teste falha).
  2. Segundo, testes pesados e difíceis de manter: Como o StandaloneMockMvcBuilder permite configurar praticamente qualquer componente do Spring MVC, pode ser tentador pendurar tudo nele por padrão. Se fizermos isso, nossos testes deixarão de ser simples testes unitários e ficarão difíceis de manter sempre que a configuração global mudar.

Por isso, devemos minimizar o número de componentes customizados incluídos no Sistema Sob Teste e não devemos copiar cegamente a mesma configuração para todas as classes de teste, pois diferentes controladores têm necessidades diferentes!

Aqui está a lista enxuta de componentes tipicamente utilizados ao escrever testes unitários para controladores Spring MVC tradicionais (aqueles que processam requisições HTTP renderizando uma view ou redirecionando):

  • Configuramos o HandlerExceptionResolver (ExceptionResolver) se nossa aplicação renderiza páginas de erro específicas quando um método do controlador lança uma exceção.
  • Especificamos o LocaleResolver se um objeto Locale for injetado como parâmetro em algum método do controlador testado.
  • Especificamos o ViewResolver caso não queiramos usar o resolvedor interno padrão do Spring MVC Test (por exemplo, para evitar erro de referência circular quando o nome da view coincide com a URL da requisição).

[!NOTE] Nota Importante sobre o ViewResolver: Na maioria das vezes, escrevemos asserções apenas para o nome lógico da view retornada pelo controlador (ex.: "task/list"), mas configurar um ViewResolver explícito no standaloneSetup garante que o MockMvc saiba diferenciar perfeitamente o caminho da view da URL da requisição.

Vamos descobrir agora como criar e configurar esses três componentes utilizando métodos fábrica na nossa classe WebTestConfig!


Criando os Componentes Necessários em WebTestConfig

Criamos os componentes necessários adicionando três métodos fábrica ao nosso objeto utilitário WebTestConfig:

  1. exceptionResolver(): Cria e retorna um objeto SimpleMappingExceptionResolver que seleciona a view de erro renderizada com base no tipo da exceção lançada (TaskNotFoundException -> "error/404", Exception / RuntimeException -> "error/error") e define o código de status HTTP retornado quando cada view de erro é renderizada (404 e 500).
  2. fixedLocaleResolver(): Retorna um novo objeto FixedLocaleResolver que sempre retorna Locale.ENGLISH. Usar um Locale fixo em testes é uma excelente prática porque garante que o idioma do sistema operacional da máquina onde os testes estão rodando jamais cause falsos positivos!
  3. jspViewResolver() (ou viewResolver()): Cria um objeto InternalResourceViewResolver configurando prefixo e sufixo de resolução de views.

Depois de adicionarmos esses três métodos fábrica à nossa classe Object Mother WebTestConfig, seu código-fonte completo em Kotlin 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
44
45
46
47
48
49
package com.testwithspring.starter.spring.web

import org.springframework.web.servlet.LocaleResolver
import org.springframework.web.servlet.ViewResolver
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver
import org.springframework.web.servlet.i18n.FixedLocaleResolver
import org.springframework.web.servlet.view.InternalResourceViewResolver
import org.springframework.web.servlet.view.JstlView
import java.util.Locale
import java.util.Properties

object WebTestConfig {

    @JvmStatic
    fun exceptionResolver(): SimpleMappingExceptionResolver {
        val exceptionResolver = SimpleMappingExceptionResolver()

        val viewMappings = Properties()
        viewMappings["com.testwithspring.starter.spring.task.TaskNotFoundException"] = "error/404"
        viewMappings["java.lang.Exception"] = "error/error"
        viewMappings["java.lang.RuntimeException"] = "error/error"

        exceptionResolver.setExceptionMappings(viewMappings)

        val statusCodes = Properties()
        statusCodes["error/404"] = "404"
        statusCodes["error/error"] = "500"

        exceptionResolver.setStatusCodes(statusCodes)

        return exceptionResolver
    }

    @JvmStatic
    fun fixedLocaleResolver(): LocaleResolver {
        return FixedLocaleResolver(Locale.ENGLISH)
    }

    @JvmStatic
    fun jspViewResolver(): ViewResolver {
        val viewResolver = InternalResourceViewResolver()

        viewResolver.setViewClass(JstlView::class.java)
        viewResolver.setPrefix("/WEB-INF/jsp/")
        viewResolver.setSuffix(".jsp")

        return viewResolver
    }
}

Agora vamos descobrir como incluir esses componentes na configuração do nosso Sistema Sob Teste!


Configurando o Sistema Sob Teste (TaskCrudControllerTest)

Antes de configurarmos o Sistema Sob Teste usando a configuração standalone, precisamos identificar as dependências do controlador que será testado.

Suponha que estejamos escrevendo testes unitários para a classe TaskCrudController, cujo código-fonte estrutural é o seguinte:

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

import org.springframework.context.MessageSource
import org.springframework.stereotype.Controller

@Controller
class TaskCrudController(
    private val crudService: TaskCrudService,
    private val messageSource: MessageSource
) {
    // Os métodos de endpoint serão analisados nas próximas aulas
}

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

  1. O objeto TaskCrudService, que fornece as operações CRUD para tarefas.
  2. O objeto MessageSource, que é utilizado para buscar mensagens internacionalizadas de feedback exibidas ao usuário.

Depois de identificarmos as dependências do controlador testado, precisamos criá-las no método de setup (@BeforeEach) que é executado antes de cada método de teste seguindo estes passos:

  1. Adicionamos as propriedades crudService: TaskCrudService e messageSource: StaticMessageSource à nossa classe de teste.
  2. Adicionamos o método @BeforeEach fun configureSystemUnderTest() à nossa classe de teste.
  3. Criamos um novo mock de TaskCrudService (mockk(relaxed = true)) e atribuímos à propriedade crudService.
  4. Criamos uma nova instância de StaticMessageSource e atribuímos à propriedade messageSource. Utilizamos a classe StaticMessageSource (fornecida pelo próprio Spring Framework!) porque ela é um Fake em memória oficial do MessageSource que nos permite registrar mensagens programaticamente com messageSource.addMessage(...) — o que é muito mais limpo do que fazer stubbing manual de métodos sobrecarregados de MessageSource!

Após criarmos as dependências do controlador testado, 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
package com.testwithspring.starter.spring.web

import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.springframework.context.support.StaticMessageSource

@Tag("unitTest")
class TaskCrudControllerTest {

    private lateinit var crudService: TaskCrudService
    private lateinit var messageSource: StaticMessageSource

    @BeforeEach
    fun configureSystemUnderTest() {
        crudService = mockk(relaxed = true)
        messageSource = StaticMessageSource()
    }
}

Nosso próximo passo é instanciar o TaskCrudController, criar o objeto MockMvc e registrar os componentes de WebTestConfig que são exigidos pelos testes desse controlador:

  • Incluímos o jspViewResolver() via .setViewResolvers(...).
  • Incluímos o exceptionResolver() via .setHandlerExceptionResolvers(...) porque queremos garantir que a view de erro correta (error/404) seja renderizada e o status HTTP 404 seja retornado quando um método do controlador lançar TaskNotFoundException.
  • Incluímos o fixedLocaleResolver() via .setLocaleResolver(...) porque um objeto Locale é injetado como parâmetro em alguns métodos do controlador para buscar mensagens no MessageSource.

Depois de criarmos o objeto MockMvc e incluirmos esses três componentes no Sistema Sob Teste, o código-fonte completo da configuração de TaskCrudControllerTest 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
package com.testwithspring.starter.spring.web

import com.testwithspring.starter.spring.web.WebTestConfig.exceptionResolver
import com.testwithspring.starter.spring.web.WebTestConfig.fixedLocaleResolver
import com.testwithspring.starter.spring.web.WebTestConfig.jspViewResolver
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.springframework.context.support.StaticMessageSource
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.MockMvcBuilders

@Tag("unitTest")
class TaskCrudControllerTest {

    private lateinit var crudService: TaskCrudService
    private lateinit var messageSource: StaticMessageSource
    private lateinit var mockMvc: MockMvc

    @BeforeEach
    fun configureSystemUnderTest() {
        crudService = mockk(relaxed = true)
        messageSource = StaticMessageSource()

        val controller = TaskCrudController(
            crudService = crudService,
            messageSource = messageSource
        )
        mockMvc = MockMvcBuilders.standaloneSetup(controller)
            .setHandlerExceptionResolvers(exceptionResolver())
            .setLocaleResolver(fixedLocaleResolver())
            .setViewResolvers(jspViewResolver())
            .build()
    }
}

Resumo

Nesta aula, aprendemos dois pontos essenciais:

  • Devemos minimizar o número de componentes customizados na configuração do MockMvc e registrar apenas aqueles que são efetivamente utilizados pelo controlador sob teste.
  • Incluímos componentes customizados (HandlerExceptionResolver, LocaleResolver, ViewResolver) no Sistema Sob Teste utilizando os métodos fluentes da classe StandaloneMockMvcBuilder alimentados pela nossa classe utilitária WebTestConfig.