Ao concluirmos esta aula, nós:
- Saberemos como criar requisições HTTP (
GET) que são processadas pelos métodos do controlador testado.
- Seremos capazes de garantir que o código de status HTTP correto (
200 OK) seja retornado.
- Seremos capazes de verificar que a view correta (
task/list, task/view) seja renderizada.
- Seremos capazes de garantir que os atributos adicionados ao
Model do Spring MVC contenham exatamente as informações esperadas.
[!NOTE]
Esta aula assume que:
Aprendendo os Fundamentos
Antes de escrevermos testes unitários para um controlador Spring MVC, precisamos saber como enviar uma requisição HTTP ao controlador testado e como escrever asserções sobre a resposta retornada:
- Primeiro, criamos requisições HTTP utilizando os métodos fábrica estáticos fornecidos pela classe
MockMvcRequestBuilders (get(), post(), put(), delete(), patch()). Esses métodos retornam um MockHttpServletRequestBuilder (ou MockMultipartHttpServletRequestBuilder), com o qual podemos configurar o corpo da requisição, adicionar cookies ou cabeçalhos HTTP e definir parâmetros de requisição. Depois de construir a requisição HTTP, nós a enviamos ao controlador testado invocando o método perform() da classe MockMvc.
- Segundo, escrevemos asserções para a resposta retornada invocando o método
andExpect() da interface ResultActions (retornada por perform()). Esse método recebe um objeto ResultMatcher criado pelos métodos fábrica estáticos da classe MockMvcResultMatchers — como status(), view() e model(). Além disso, dentro de model().attribute(...), podemos combinar matchers do Hamcrest (hasSize, contains, allOf, hasProperty, is) para inspecionar profundamente os objetos do Model!
Vamos avançar e ver na prática como utilizar essas classes ao escrever testes unitários para controladores Spring MVC que leem informações do banco de dados.
Teste 1: Renderizando Todas as Tarefas Encontradas no Banco de Dados (showTaskList)
Precisamos escrever testes unitários para um método de controlador que processa requisições GET enviadas para o caminho "/". Esse método se chama showTaskList() e é implementado seguindo estes três passos:
- Busca todas as tarefas no banco de dados utilizando o método
findAll() da interface TaskCrudService.
- Coloca as tarefas encontradas em um atributo do
Model chamado "tasks".
- Retorna o nome lógico da view (
"task/list") que renderiza a lista de tarefas.
O código-fonte do método do controlador testado é o seguinte:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package com.testwithspring.starter.spring.web
import org.springframework.context.MessageSource
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
@Controller
class TaskCrudController(
private val crudService: TaskCrudService,
private val messageSource: MessageSource
) {
@GetMapping("/")
fun showTaskList(model: Model): String {
val tasks: List<TaskListDTO> = crudService.findAll()
model.addAttribute("tasks", tasks)
return "task/list"
}
}
|
A classe TaskListDTO é um DTO simples que contém as informações resumidas exibidas na página de listagem de tarefas:
1
2
3
4
5
6
7
|
package com.testwithspring.starter.spring.web
class TaskListDTO {
var id: Long? = null
var title: String? = null
var status: TaskStatus? = null
}
|
A seguir, escreveremos testes unitários que garantem que o método showTaskList() funciona como esperado quando duas tarefas são encontradas. Podemos escrever esses testes seguindo quatro passos:
Passo 1: Configurar o Sistema Sob Teste no @BeforeEach
Primeiro, configuramos o Sistema Sob Teste no método anotado com @BeforeEach na raiz da classe TaskCrudControllerTest. Para este exemplo inicial, não precisamos sequer registrar componentes extras além do próprio controlador no standaloneSetup:
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
|
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
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)
.build()
}
}
|
[!NOTE]
Neste teste de leitura, crudService atua como um Stub, mas usamos mockk() porque os demais testes desta mesma classe TaskCrudControllerTest (que veremos na próxima aula, sobre formulários) verificarão interações com verify { ... }.
Passo 2: Criar a hierarquia de classes internas (@Nested)
Segundo, criamos a hierarquia de classes aninhadas (ShowTaskList -> WhenTwoTasksAreFound) para organizar nossos casos de teste:
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
|
package com.testwithspring.starter.spring.web
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
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)
.build()
}
@Nested
inner class ShowTaskList {
@Nested
inner class WhenTwoTasksAreFound {
// Adicionaremos os testes aqui
}
}
}
|
Passo 3: Configurar o cenário em WhenTwoTasksAreFound
Terceiro, configuramos nossos casos de teste dentro da classe WhenTwoTasksAreFound seguindo estes passos:
- Adicionamos duas propriedades
TaskListDTO (first e second) à classe WhenTwoTasksAreFound.
- Adicionamos um método fábrica privado chamado
createTask() dentro de WhenTwoTasksAreFound que cria e retorna um objeto TaskListDTO.
- Adicionamos um método
@BeforeEach fun returnTwoTasks() à classe WhenTwoTasksAreFound.
- Implementamos
returnTwoTasks() criando dois objetos TaskListDTO e configurando crudService.findAll() com every { ... } returns listOf(first, second).
Após configurarmos o cenário, 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
package com.testwithspring.starter.spring.web
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
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)
.build()
}
@Nested
inner class ShowTaskList {
@Nested
inner class WhenTwoTasksAreFound {
private lateinit var first: TaskListDTO
private lateinit var second: TaskListDTO
@BeforeEach
fun returnTwoTasks() {
first = createTask(1L, "title", TaskStatus.OPEN)
second = createTask(33L, "secondTask", TaskStatus.OPEN)
every { crudService.findAll() } returns listOf(first, second)
}
private fun createTask(
id: Long,
title: String,
status: TaskStatus
): TaskListDTO {
val task = TaskListDTO()
task.id = id
task.title = title
task.status = status
return task
}
}
}
}
|
Passo 4: Escrever os cinco testes unitários para showTaskList()
Quarto, escrevemos cinco métodos de teste unitário focados (um conceito lógico por teste!) para o método showTaskList():
- Verificamos que ele retorna o código de status HTTP
200 OK (status().isOk).
- Garantimos que ele renderiza a view de listagem de tarefas (
view().name("task/list")).
- Verificamos que a lista de tarefas no atributo
"tasks" do Model contém exatamente duas tarefas (hasSize(2)).
- Garantimos que as duas tarefas estão na ordem exata retornada pelo serviço (
contains(first, second)).
- Verificamos que os objetos da lista renderizada contêm as propriedades corretas (
id, title, status).
Depois de escrevermos esses cinco testes unitários, o código-fonte completo 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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
package com.testwithspring.starter.spring.web
import io.mockk.every
import io.mockk.mockk
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.contains
import org.hamcrest.Matchers.hasItem
import org.hamcrest.Matchers.hasProperty
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is`
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.springframework.context.support.StaticMessageSource
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.model
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.view
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)
.build()
}
@Nested
inner class ShowTaskList {
@Nested
inner class WhenTwoTasksAreFound {
private lateinit var first: TaskListDTO
private lateinit var second: TaskListDTO
@BeforeEach
fun returnTwoTasks() {
first = createTask(1L, "title", TaskStatus.OPEN)
second = createTask(33L, "secondTask", TaskStatus.OPEN)
every { crudService.findAll() } returns listOf(first, second)
}
private fun createTask(
id: Long,
title: String,
status: TaskStatus
): TaskListDTO {
val task = TaskListDTO()
task.id = id
task.title = title
task.status = status
return task
}
@Test
fun shouldReturnHttpStatusCodeOk() {
mockMvc.perform(get("/"))
.andExpect(status().isOk)
}
@Test
fun shouldRenderTaskListView() {
mockMvc.perform(get("/"))
.andExpect(view().name("task/list"))
}
@Test
fun shouldShowTaskListThatHasTwoTasks() {
mockMvc.perform(get("/"))
.andExpect(model().attribute("tasks", hasSize<Any>(2)))
}
@Test
fun shouldShowTwoTasksInCorrectOrder() {
mockMvc.perform(get("/"))
.andExpect(model().attribute("tasks", contains(first, second)))
}
@Test
fun shouldShowCorrectInformation() {
mockMvc.perform(get("/"))
.andExpect(
model().attribute(
"tasks",
allOf(
hasItem(
allOf(
hasProperty("id", `is`(1L)),
hasProperty("title", `is`("title")),
hasProperty("status", `is`(TaskStatus.OPEN))
)
),
hasItem(
allOf(
hasProperty("id", `is`(33L)),
hasProperty("title", `is`("secondTask")),
hasProperty("status", `is`(TaskStatus.OPEN))
)
)
)
)
)
}
}
}
}
|
Vamos avançar e escrever testes unitários para um método de controlador que renderiza as informações detalhadas de uma única tarefa!
Agora precisamos escrever testes unitários para um método de controlador que processa requisições GET enviadas para o caminho "/task/{taskId}". Esse método se chama showTask() e é implementado seguindo estes três passos:
- Busca a tarefa no banco de dados invocando o método
findById() da interface TaskCrudService, passando o taskId recebido como variável de caminho (@PathVariable("taskId")).
- Coloca a tarefa encontrada em um atributo do
Model chamado "task".
- Retorna o nome da view (
"task/view") que renderiza os detalhes da tarefa encontrada.
O código-fonte do método showTask() no controlador é o seguinte:
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.spring.web
import org.springframework.context.MessageSource
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
@Controller
class TaskCrudController(
private val crudService: TaskCrudService,
private val messageSource: MessageSource
) {
@GetMapping("/task/{taskId}")
fun showTask(
@PathVariable("taskId") taskId: Long,
model: Model
): String {
val task: TaskDTO = crudService.findById(taskId)
model.addAttribute("task", task)
return "task/view"
}
}
|
A classe TaskDTO é um DTO que contém todas as informações detalhadas de uma única tarefa:
1
2
3
4
5
6
7
8
9
10
11
12
|
package com.testwithspring.starter.spring.web
class TaskDTO {
var assigneeId: Long? = null
var closerId: Long? = null
var creatorId: Long? = null
var description: String? = null
var id: Long? = null
var resolution: TaskResolution? = null
var status: TaskStatus? = null
var title: String? = null
}
|
Vamos escrever os testes unitários que garantem que o método showTask() funciona como esperado quando a tarefa solicitada é encontrada, seguindo três passos:
Passo 1: Adicionar a hierarquia de classes internas ShowTask -> WhenTaskIsFound
Primeiro, adicionamos a classe interna ShowTask e, dentro dela, a classe interna WhenTaskIsFound na nossa classe TaskCrudControllerTest:
1
2
3
4
5
6
7
8
9
|
@Nested
inner class ShowTask {
@Nested
inner class WhenTaskIsFound {
// Adicionaremos os testes aqui
}
}
|
Segundo, configuramos nossos casos de teste dentro de WhenTaskIsFound seguindo estes passos:
- Adicionamos um método
@BeforeEach fun returnFoundTask() à classe WhenTaskIsFound.
- Criamos um novo objeto
TaskDTO utilizando um Test Data Builder (TaskDTOBuilder), pois o estado de um TaskDTO depende de invariantes de domínio (como estar aberto com status = OPEN, resolution = null e closerId = null).
- Configuramos
crudService.findById(1L) com every { ... } returns found.
Após configurarmos o método returnFoundTask(), a classe ShowTask fica assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Nested
inner class ShowTask {
@Nested
inner class WhenTaskIsFound {
@BeforeEach
fun returnFoundTask() {
val found = TaskDTOBuilder()
.withId(1L)
.withCreator(99L)
.withTitle("title")
.withDescription("description")
.withStatusOpen()
.build()
every { crudService.findById(1L) } returns found
}
}
}
|
Passo 3: Escrever os três testes unitários para showTask()
Terceiro, escrevemos três testes unitários garantindo que o método showTask() funciona como esperado quando enviamos uma requisição GET para "/task/1" (get("/task/{taskId}", 1L)):
- Verificamos que o código de status HTTP
200 OK é retornado.
- Garantimos que o método
showTask() renderiza a view "task/view".
- Verificamos que o atributo
"task" do Model contém todas as propriedades esperadas da tarefa encontrada.
O código-fonte completo da classe TaskCrudControllerTest com os testes de ShowTask 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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
package com.testwithspring.starter.spring.web
import io.mockk.every
import io.mockk.mockk
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.hasProperty
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.nullValue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.springframework.context.support.StaticMessageSource
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.model
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.view
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)
.build()
}
@Nested
inner class ShowTask {
@Nested
inner class WhenTaskIsFound {
@BeforeEach
fun returnFoundTask() {
val found = TaskDTOBuilder()
.withId(1L)
.withCreator(99L)
.withTitle("title")
.withDescription("description")
.withStatusOpen()
.build()
every { crudService.findById(1L) } returns found
}
@Test
fun shouldReturnHttpStatusCodeOk() {
mockMvc.perform(get("/task/{taskId}", 1L))
.andExpect(status().isOk)
}
@Test
fun shouldRenderViewTaskView() {
mockMvc.perform(get("/task/{taskId}", 1L))
.andExpect(view().name("task/view"))
}
@Test
fun shouldShowFoundTask() {
mockMvc.perform(get("/task/{taskId}", 1L))
.andExpect(
model().attribute(
"task",
allOf(
hasProperty("assigneeId", nullValue()),
hasProperty("closerId", nullValue()),
hasProperty("creatorId", `is`(99L)),
hasProperty("id", `is`(1L)),
hasProperty("title", `is`("title")),
hasProperty("description", `is`("description")),
hasProperty("status", `is`(TaskStatus.OPEN)),
hasProperty("resolution", nullValue())
)
)
)
}
}
}
}
|
Resumo
Nesta aula, aprendemos três pontos fundamentais:
- Criamos requisições HTTP (
get("/"), get("/task/{taskId}", 1L)) utilizando os métodos fábrica estáticos da classe MockMvcRequestBuilders.
- Escrevemos asserções para a resposta HTTP retornada (
status().isOk, view().name(...), model().attribute(...)) utilizando os métodos fábrica estáticos da classe MockMvcResultMatchers.
- Escrevemos asserções expressivas sobre coleções e objetos dentro do
Model combinando matchers do Hamcrest (hasSize, contains, allOf, hasItem, hasProperty, is, nullValue).