Ao concluirmos esta aula, nós:
- Seremos capazes de escrever asserções para o tipo de conteúdo (
Content-Type) da resposta HTTP.
- Saberemos como escrever asserções para o documento JSON retornado pela API utilizando expressões JsonPath combinadas com matchers do Hamcrest ou
MockMvcTester.
Cenário 1: Retornando Todas as Tarefas Encontradas no Banco de Dados
Nosso primeiro exemplo analisa um endpoint REST que retorna todas as tarefas cadastradas no banco de dados. Ele utiliza a classe TaskListDTO, cujos campos são serializados em JSON quando o controlador processa uma requisição GET /api/task:
1
2
3
4
5
6
7
|
package com.testwithspring.starter.web
data class TaskListDTO(
var id: Long? = null,
var title: String? = null,
var status: TaskStatus? = null
)
|
O método handler findAll() da classe TaskCrudController delega a busca ao TaskCrudService e retorna a lista encontrada:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package com.testwithspring.starter.web
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/task")
class TaskCrudController(
private val crudService: TaskCrudService
) {
@GetMapping
fun findAll(): List<TaskListDTO> {
return crudService.findAll()
}
}
|
A estrutura base da nossa classe de teste (TaskCrudControllerTest) está configurada da seguinte maneira:
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
|
package com.testwithspring.starter.web
import com.testwithspring.starter.web.WebTestConfig.objectMapperHttpMessageConverter
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
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()
}
@Nested
@DisplayName("Buscar todas as tarefas (GET /api/task)")
inner class FindAll {
// Os casos de teste ficarão aqui
}
}
|
Precisamos escrever testes unitários para dois casos:
- Quando nenhuma tarefa é encontrada no banco de dados (
crudService.findAll() retorna uma lista vazia).
- Quando duas tarefas são encontradas no banco de dados.
Vamos implementar cada um desses cenários passo a passo!
Caso 1.1: Quando Nenhuma Tarefa É Encontrada
Quando o banco de dados não possui tarefas, nosso endpoint REST deve:
- Retornar o código de status HTTP 200 (
OK).
- Retornar uma resposta com o cabeçalho
Content-Type igual a application/json.
- Retornar um array JSON vazio (
[]), ou seja, uma coleção JSON com tamanho 0.
Nota sobre MediaType.APPLICATION_JSON no Spring Boot moderno: No antigo Spring Framework 4.2/4.3, usava-se MediaType.APPLICATION_JSON_UTF8. Desde o Spring Framework 5.2+ e no Spring Boot 4.1 (Spring Framework 7), APPLICATION_JSON_UTF8 foi depreciado porque a especificação oficial RFC 8259 define que UTF-8 já é o encoding padrão obrigatório de todo documento application/json. Portanto, sempre verificamos MediaType.APPLICATION_JSON!
Vamos escrever a classe aninhada WhenNoTasksAreFound passo a passo:
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
|
@Nested
@DisplayName("Buscar todas as tarefas (GET /api/task)")
inner class FindAll {
@Nested
@DisplayName("Quando nenhuma tarefa é encontrada")
inner class WhenNoTasksAreFound {
@BeforeEach
fun returnEmptyTaskList() {
every { crudService.findAll() } returns emptyList()
}
@Test
@DisplayName("Deve retornar o status HTTP 200 OK")
fun shouldReturnHttpStatusCodeOk() {
mockMvc.perform(get("/api/task"))
.andExpect(status().isOk)
}
@Test
@DisplayName("Deve retornar as tarefas encontradas como JSON")
fun shouldReturnFoundTasksAsJson() {
mockMvc.perform(get("/api/task"))
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
}
@Test
@DisplayName("Deve retornar zero tarefas no array JSON")
fun shouldReturnZeroTasks() {
mockMvc.perform(get("/api/task"))
.andExpect(jsonPath("$", hasSize<Any>(0)))
}
}
}
|
Observe como funcionam os dois novos matchers de resposta que utilizamos:
content().contentType(MediaType.APPLICATION_JSON): Verifica que o cabeçalho Content-Type da resposta HTTP é application/json.
jsonPath("$", hasSize<Any>(0)): A expressão JsonPath $ refere-se ao elemento raiz do documento JSON (que neste caso é um array [...]). Combinada com o matcher Matchers.hasSize(0) do Hamcrest, ela verifica que o array JSON retornado está vazio!
Caso 1.2: Quando Duas Tarefas São Encontradas
Quando o método crudService.findAll() retorna duas tarefas, o documento JSON retornado pelo controlador tem o seguinte formato:
1
2
3
4
5
6
7
8
9
10
11
12
|
[
{
"id": 1,
"title": "Primeira tarefa",
"status": "OPEN"
},
{
"id": 2,
"title": "Segunda tarefa",
"status": "OPEN"
}
]
|
Nossa suíte de testes para este cenário deve garantir que o endpoint:
- Retorne o código de status HTTP 200 (
OK).
- Retorne o tipo de conteúdo
application/json.
- Retorne um array JSON contendo exatamente 2 tarefas (
hasSize(2)).
- Retorne as informações corretas da primeira tarefa no índice
$[0] e da segunda tarefa no índice $[1].
Vamos implementar a classe aninhada WhenTwoTasksAreFound:
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
|
@Nested
@DisplayName("Quando duas tarefas são encontradas")
inner class WhenTwoTasksAreFound {
private val firstTaskId = 1L
private val firstTaskTitle = "Primeira tarefa"
private val secondTaskId = 2L
private val secondTaskTitle = "Segunda tarefa"
@BeforeEach
fun returnTwoTasks() {
val first = TaskListDTO(
id = firstTaskId,
title = firstTaskTitle,
status = TaskStatus.OPEN
)
val second = TaskListDTO(
id = secondTaskId,
title = secondTaskTitle,
status = TaskStatus.OPEN
)
every { crudService.findAll() } returns listOf(first, second)
}
@Test
@DisplayName("Deve retornar o status HTTP 200 OK")
fun shouldReturnHttpStatusCodeOk() {
mockMvc.perform(get("/api/task"))
.andExpect(status().isOk)
}
@Test
@DisplayName("Deve retornar as tarefas encontradas como JSON")
fun shouldReturnFoundTasksAsJson() {
mockMvc.perform(get("/api/task"))
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
}
@Test
@DisplayName("Deve retornar duas tarefas")
fun shouldReturnTwoTasks() {
mockMvc.perform(get("/api/task"))
.andExpect(jsonPath("$", hasSize<Any>(2)))
}
@Test
@DisplayName("Deve retornar as informações das tarefas encontradas")
fun shouldReturnInformationOfFoundTasks() {
mockMvc.perform(get("/api/task"))
.andExpect(jsonPath("$[0].id", equalTo(firstTaskId.toInt())))
.andExpect(jsonPath("$[0].title", equalTo(firstTaskTitle)))
.andExpect(jsonPath("$[0].status", equalTo(TaskStatus.OPEN.name)))
.andExpect(jsonPath("$[1].id", equalTo(secondTaskId.toInt())))
.andExpect(jsonPath("$[1].title", equalTo(secondTaskTitle)))
.andExpect(jsonPath("$[1].status", equalTo(TaskStatus.OPEN.name)))
}
}
|
Atenção a dois detalhes importantes do JsonPath:
- Números Inteiros no JsonPath: Por padrão, o Jayway JsonPath desserializa números inteiros pequenos em JSON como
java.lang.Integer (Int), mesmo que no lado servidor a propriedade Kotlin seja Long. Por isso, ao comparar o ID com equalTo(...) do Hamcrest, usamos firstTaskId.toInt() (ou passamos diretamente 1 / 2).
- Enums em JSON: O Jackson serializa enums (
TaskStatus.OPEN) como String contendo o .name da constante ("OPEN"). Por isso, ao escrever a asserção com jsonPath("$[0].status", ...), devemos comparar com TaskStatus.OPEN.name ("OPEN") e não com a instância do enum!
Nosso segundo exemplo analisa o endpoint GET /api/task/{id}, que retorna todos os detalhes de uma tarefa específica usando a classe TaskDTO:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package com.testwithspring.starter.web
import java.time.Instant
data class TaskDTO(
var id: Long? = null,
var title: String? = null,
var description: String? = null,
var status: TaskStatus? = null,
var resolution: TaskResolution? = null,
var creatorId: Long? = null,
var assigneeId: Long? = null,
var closerId: Long? = null,
var creator: PersonDTO? = null,
var assignee: PersonDTO? = null,
var closer: PersonDTO? = null,
var modifier: PersonDTO? = null,
var creationTime: Instant? = null,
var modificationTime: Instant? = null,
var tags: List<TagDTO> = emptyList()
)
|
Onde PersonDTO e TagDTO são definidos em:
1
2
3
4
5
6
7
8
9
10
11
|
package com.testwithspring.starter.web
data class PersonDTO(
val userId: Long,
val name: String
)
data class TagDTO(
val id: Long,
val name: String
)
|
E o controlador TaskCrudController possui o método findById() e um @ExceptionHandler que retorna o status HTTP 404 (Not Found) quando a tarefa solicitada não existe:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@RestController
@RequestMapping("/api/task")
class TaskCrudController(
private val crudService: TaskCrudService
) {
@GetMapping("/{id}")
fun findById(@PathVariable("id") taskId: Long): TaskDTO {
return crudService.findById(taskId)
}
@ExceptionHandler(NotFoundException::class)
@ResponseStatus(HttpStatus.NOT_FOUND)
fun handleNotFoundError() {
}
}
|
Precisamos testar dois cenários:
- Quando a tarefa solicitada não é encontrada (
crudService.findById(taskId) lança NotFoundException).
- Quando a tarefa solicitada é encontrada (validando campos simples, timestamps ISO-8601, objetos aninhados e listas de tags).
Caso 2.1: Quando a Tarefa Solicitada Não É Encontrada
Quando a tarefa não existe, o endpoint deve retornar o código de status HTTP 404 (Not Found):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Nested
@DisplayName("Buscar tarefa pelo ID (GET /api/task/{id})")
inner class FindById {
private val taskId = 99L
@Nested
@DisplayName("Quando a tarefa solicitada não é encontrada")
inner class WhenRequestedTaskIsNotFound {
@BeforeEach
fun throwException() {
every { crudService.findById(taskId) } throws NotFoundException("Tarefa $taskId não encontrada")
}
@Test
@DisplayName("Deve retornar o status HTTP 404 Not Found")
fun shouldReturnHttpStatusCodeNotFound() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(status().isNotFound)
}
}
|
Caso 2.2: Quando a Tarefa Solicitada É Encontrada
Quando a tarefa é encontrada, o endpoint retorna um documento JSON rico que inclui objetos aninhados (assignee, closer, creator, modifier), datas no padrão ISO-8601 (creationTime, modificationTime) e uma lista de tags (tags):
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
|
{
"id": 99,
"assignee": {
"userId": 1,
"name": "Responsável"
},
"closer": {
"userId": 2,
"name": "Finalizador"
},
"creationTime": "2026-11-01T09:00:00Z",
"creator": {
"userId": 3,
"name": "Criador"
},
"description": "Descrição detalhada da tarefa",
"modificationTime": "2026-11-01T10:30:00Z",
"modifier": {
"userId": 4,
"name": "Modificador"
},
"resolution": "DONE",
"status": "CLOSED",
"tags": [
{
"id": 44,
"name": "backend"
}
],
"title": "Implementar endpoint GET"
}
|
Vamos escrever a classe aninhada WhenRequestedTaskIsFound testando cada grupo lógico de propriedades do JSON:
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
|
@Nested
@DisplayName("Quando a tarefa solicitada é encontrada")
inner class WhenRequestedTaskIsFound {
private val assigneeId = 1L
private val assigneeName = "Responsável"
private val closerId = 2L
private val closerName = "Finalizador"
private val creationTimeString = "2026-11-01T09:00:00Z"
private val creationTime = Instant.parse(creationTimeString)
private val creatorId = 3L
private val creatorName = "Criador"
private val description = "Descrição detalhada da tarefa"
private val modificationTimeString = "2026-11-01T10:30:00Z"
private val modificationTime = Instant.parse(modificationTimeString)
private val modifierId = 4L
private val modifierName = "Modificador"
private val tagId = 44L
private val tagName = "backend"
private val title = "Implementar endpoint GET"
@BeforeEach
fun returnFoundTask() {
val found = TaskDTO(
id = taskId,
assignee = PersonDTO(userId = assigneeId, name = assigneeName),
closer = PersonDTO(userId = closerId, name = closerName),
creationTime = creationTime,
creator = PersonDTO(userId = creatorId, name = creatorName),
description = description,
modificationTime = modificationTime,
modifier = PersonDTO(userId = modifierId, name = modifierName),
resolution = TaskResolution.DONE,
status = TaskStatus.CLOSED,
tags = listOf(TagDTO(id = tagId, name = tagName)),
title = title
)
every { crudService.findById(taskId) } returns found
}
@Test
@DisplayName("Deve retornar o status HTTP 200 OK")
fun shouldReturnHttpStatusCodeOk() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(status().isOk)
}
@Test
@DisplayName("Deve retornar a tarefa encontrada como JSON")
fun shouldReturnFoundTaskAsJson() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
}
@Test
@DisplayName("Deve retornar as informações do responsável e do finalizador")
fun shouldReturnInformationOfAssigneeAndCloser() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.assignee.userId", equalTo(assigneeId.toInt())))
.andExpect(jsonPath("$.assignee.name", equalTo(assigneeName)))
.andExpect(jsonPath("$.closer.userId", equalTo(closerId.toInt())))
.andExpect(jsonPath("$.closer.name", equalTo(closerName)))
}
@Test
@DisplayName("Deve retornar as informações de auditoria de criação")
fun shouldReturnCreationInformation() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.creationTime", equalTo(creationTimeString)))
.andExpect(jsonPath("$.creator.userId", equalTo(creatorId.toInt())))
.andExpect(jsonPath("$.creator.name", equalTo(creatorName)))
}
@Test
@DisplayName("Deve retornar o ID, título e descrição da tarefa")
fun shouldReturnIdTitleAndDescription() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.id", equalTo(taskId.toInt())))
.andExpect(jsonPath("$.title", equalTo(title)))
.andExpect(jsonPath("$.description", equalTo(description)))
}
@Test
@DisplayName("Deve retornar as informações de auditoria de modificação")
fun shouldReturnModificationInformation() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.modificationTime", equalTo(modificationTimeString)))
.andExpect(jsonPath("$.modifier.userId", equalTo(modifierId.toInt())))
.andExpect(jsonPath("$.modifier.name", equalTo(modifierName)))
}
@Test
@DisplayName("Deve retornar o status e a resolução da tarefa")
fun shouldReturnStatusAndResolution() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.resolution", equalTo(TaskResolution.DONE.name)))
.andExpect(jsonPath("$.status", equalTo(TaskStatus.CLOSED.name)))
}
@Test
@DisplayName("Deve retornar a lista de tags da tarefa")
fun shouldReturnTagsOfTask() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.tags", hasSize<Any>(1)))
.andExpect(jsonPath("$.tags[0].id", equalTo(tagId.toInt())))
.andExpect(jsonPath("$.tags[0].name", equalTo(tagName)))
}
}
}
|
Código Completo da Suíte de Testes de Leitura REST (GET)
Abaixo apresentamos a classe completa TaskRestControllerGetTest em nosso repositório real de testes:
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
|
package com.testwithspring.starter.web
import com.testwithspring.starter.web.WebTestConfig.objectMapperHttpMessageConverter
import io.mockk.every
import io.mockk.mockk
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.hasSize
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.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import java.time.Instant
@Tag("unitTest")
@DisplayName("01-07-07: Testes Unitários de API REST - Leitura de Dados (GET)")
class TaskRestControllerGetTest {
private val crudService: TaskCrudService = mockk()
private lateinit var mockMvc: MockMvc
@BeforeEach
fun configureSystemUnderTest() {
mockMvc = MockMvcBuilders.standaloneSetup(TaskRestController(crudService))
.setMessageConverters(objectMapperHttpMessageConverter())
.build()
}
@Nested
@DisplayName("Buscar todas as tarefas (GET /api/task)")
inner class FindAll {
@Nested
@DisplayName("Quando nenhuma tarefa é encontrada")
inner class WhenNoTasksAreFound {
@BeforeEach
fun returnEmptyTaskList() {
every { crudService.findAll() } returns emptyList()
}
@Test
@DisplayName("Deve retornar o status HTTP 200 OK")
fun shouldReturnHttpStatusCodeOk() {
mockMvc.perform(get("/api/task"))
.andExpect(status().isOk)
}
@Test
@DisplayName("Deve retornar as tarefas encontradas como JSON")
fun shouldReturnFoundTasksAsJson() {
mockMvc.perform(get("/api/task"))
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
}
@Test
@DisplayName("Deve retornar zero tarefas no array JSON")
fun shouldReturnZeroTasks() {
mockMvc.perform(get("/api/task"))
.andExpect(jsonPath("$", hasSize<Any>(0)))
}
}
@Nested
@DisplayName("Quando duas tarefas são encontradas")
inner class WhenTwoTasksAreFound {
private val firstTaskId = 1L
private val firstTaskTitle = "Primeira tarefa"
private val secondTaskId = 2L
private val secondTaskTitle = "Segunda tarefa"
@BeforeEach
fun returnTwoTasks() {
val first = TaskListDTO(
id = firstTaskId,
title = firstTaskTitle,
status = TaskStatus.OPEN
)
val second = TaskListDTO(
id = secondTaskId,
title = secondTaskTitle,
status = TaskStatus.OPEN
)
every { crudService.findAll() } returns listOf(first, second)
}
@Test
@DisplayName("Deve retornar o status HTTP 200 OK")
fun shouldReturnHttpStatusCodeOk() {
mockMvc.perform(get("/api/task"))
.andExpect(status().isOk)
}
@Test
@DisplayName("Deve retornar as tarefas encontradas como JSON")
fun shouldReturnFoundTasksAsJson() {
mockMvc.perform(get("/api/task"))
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
}
@Test
@DisplayName("Deve retornar duas tarefas")
fun shouldReturnTwoTasks() {
mockMvc.perform(get("/api/task"))
.andExpect(jsonPath("$", hasSize<Any>(2)))
}
@Test
@DisplayName("Deve retornar as informações das tarefas encontradas")
fun shouldReturnInformationOfFoundTasks() {
mockMvc.perform(get("/api/task"))
.andExpect(jsonPath("$[0].id", equalTo(firstTaskId.toInt())))
.andExpect(jsonPath("$[0].title", equalTo(firstTaskTitle)))
.andExpect(jsonPath("$[0].status", equalTo(TaskStatus.OPEN.name)))
.andExpect(jsonPath("$[1].id", equalTo(secondTaskId.toInt())))
.andExpect(jsonPath("$[1].title", equalTo(secondTaskTitle)))
.andExpect(jsonPath("$[1].status", equalTo(TaskStatus.OPEN.name)))
}
}
}
@Nested
@DisplayName("Buscar tarefa pelo ID (GET /api/task/{id})")
inner class FindById {
private val taskId = 99L
@Nested
@DisplayName("Quando a tarefa solicitada não é encontrada")
inner class WhenRequestedTaskIsNotFound {
@BeforeEach
fun throwException() {
every { crudService.findById(taskId) } throws TaskNotFoundException("Tarefa $taskId não encontrada")
}
@Test
@DisplayName("Deve retornar o status HTTP 404 Not Found")
fun shouldReturnHttpStatusCodeNotFound() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(status().isNotFound)
}
}
@Nested
@DisplayName("Quando a tarefa solicitada é encontrada")
inner class WhenRequestedTaskIsFound {
private val assigneeId = 1L
private val assigneeName = "Responsável"
private val closerId = 2L
private val closerName = "Finalizador"
private val creationTimeString = "2026-11-01T09:00:00Z"
private val creationTime = Instant.parse(creationTimeString)
private val creatorId = 3L
private val creatorName = "Criador"
private val description = "Descrição detalhada da tarefa"
private val modificationTimeString = "2026-11-01T10:30:00Z"
private val modificationTime = Instant.parse(modificationTimeString)
private val modifierId = 4L
private val modifierName = "Modificador"
private val tagId = 44L
private val tagName = "backend"
private val title = "Implementar endpoint GET"
@BeforeEach
fun returnFoundTask() {
val found = TaskDTO(
id = taskId,
assignee = PersonDTO(userId = assigneeId, name = assigneeName),
closer = PersonDTO(userId = closerId, name = closerName),
creationTime = creationTime,
creator = PersonDTO(userId = creatorId, name = creatorName),
description = description,
modificationTime = modificationTime,
modifier = PersonDTO(userId = modifierId, name = modifierName),
resolution = TaskResolution.DONE,
status = TaskStatus.CLOSED,
tags = listOf(TagDTO(id = tagId, name = tagName)),
title = title
)
every { crudService.findById(taskId) } returns found
}
@Test
@DisplayName("Deve retornar o status HTTP 200 OK")
fun shouldReturnHttpStatusCodeOk() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(status().isOk)
}
@Test
@DisplayName("Deve retornar a tarefa encontrada como JSON")
fun shouldReturnFoundTaskAsJson() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
}
@Test
@DisplayName("Deve retornar as informações do responsável e do finalizador")
fun shouldReturnInformationOfAssigneeAndCloser() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.assignee.userId", equalTo(assigneeId.toInt())))
.andExpect(jsonPath("$.assignee.name", equalTo(assigneeName)))
.andExpect(jsonPath("$.closer.userId", equalTo(closerId.toInt())))
.andExpect(jsonPath("$.closer.name", equalTo(closerName)))
}
@Test
@DisplayName("Deve retornar as informações de auditoria de criação")
fun shouldReturnCreationInformation() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.creationTime", equalTo(creationTimeString)))
.andExpect(jsonPath("$.creator.userId", equalTo(creatorId.toInt())))
.andExpect(jsonPath("$.creator.name", equalTo(creatorName)))
}
@Test
@DisplayName("Deve retornar o ID, título e descrição da tarefa")
fun shouldReturnIdTitleAndDescription() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.id", equalTo(taskId.toInt())))
.andExpect(jsonPath("$.title", equalTo(title)))
.andExpect(jsonPath("$.description", equalTo(description)))
}
@Test
@DisplayName("Deve retornar as informações de auditoria de modificação")
fun shouldReturnModificationInformation() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.modificationTime", equalTo(modificationTimeString)))
.andExpect(jsonPath("$.modifier.userId", equalTo(modifierId.toInt())))
.andExpect(jsonPath("$.modifier.name", equalTo(modifierName)))
}
@Test
@DisplayName("Deve retornar o status e a resolução da tarefa")
fun shouldReturnStatusAndResolution() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.resolution", equalTo(TaskResolution.DONE.name)))
.andExpect(jsonPath("$.status", equalTo(TaskStatus.CLOSED.name)))
}
@Test
@DisplayName("Deve retornar a lista de tags da tarefa")
fun shouldReturnTagsOfTask() {
mockMvc.perform(get("/api/task/{id}", taskId))
.andExpect(jsonPath("$.tags", hasSize<Any>(1)))
.andExpect(jsonPath("$.tags[0].id", equalTo(tagId.toInt())))
.andExpect(jsonPath("$.tags[0].name", equalTo(tagName)))
}
}
}
}
|
Executando Nossos Testes REST no Gradle
Ao executarmos a suíte TaskRestControllerGetTest através do Gradle (./gradlew test --tests TaskRestControllerGetTest -i), inspecionamos o log de validação com sucesso:
Initialized native services in: /home/wc/.gradle/native
Initialized jansi services in: /home/wc/.gradle/native
The client will now receive all logging from the daemon (pid: 17201, address: [fe80::1%1:54335]).
Starting Build
Settings evaluated using settings file '/home/wc/test-with-spring/code/01-starter-junit6/settings.gradle.kts'.
Projects loaded. Root project using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Included projects: [root project 'starter-junit6']
> Configure project :
Evaluating root project 'starter-junit6' using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Using Kotlin Gradle Plugin 2.4.21
Using Java Toolchain: Temurin 21.0.6 (/home/wc/.sdkman/candidates/java/21.0.6-tem)
Spring Boot 4.1.2 plugin applied.
All projects evaluated.
Selected primary task 'test' from project :
Tasks to be executed: [task ':compileKotlin', task ':compileJava', task ':processResources', task ':classes', task ':compileTestKotlin', task ':compileTestJava', task ':processTestResources', task ':testClasses', task ':test']
Tasks that were skipped: [task ':compileJava', task ':compileTestJava']
> Task :compileKotlin UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestKotlin UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test
Starting process 'Gradle Test Executor 1'. Working directory: /home/wc/test-with-spring/code/01-starter-junit6 Command: /home/wc/.sdkman/candidates/java/21.0.6-tem/bin/java -ea -cp ...
Successfully started process 'Gradle Test Executor 1'
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar todas as tarefas (GET /api/task) > Quando nenhuma tarefa é encontrada > Deve retornar o status HTTP 200 OK PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar todas as tarefas (GET /api/task) > Quando nenhuma tarefa é encontrada > Deve retornar as tarefas encontradas como JSON PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar todas as tarefas (GET /api/task) > Quando nenhuma tarefa é encontrada > Deve retornar zero tarefas no array JSON PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar todas as tarefas (GET /api/task) > Quando duas tarefas são encontradas > Deve retornar o status HTTP 200 OK PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar todas as tarefas (GET /api/task) > Quando duas tarefas são encontradas > Deve retornar as tarefas encontradas como JSON PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar todas as tarefas (GET /api/task) > Quando duas tarefas são encontradas > Deve retornar duas tarefas PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar todas as tarefas (GET /api/task) > Quando duas tarefas são encontradas > Deve retornar as informações das tarefas encontradas PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar tarefa pelo ID (GET /api/task/{id}) > Quando a tarefa solicitada não é encontrada > Deve retornar o status HTTP 404 Not Found PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar tarefa pelo ID (GET /api/task/{id}) > Quando a tarefa solicitada é encontrada > Deve retornar o status HTTP 200 OK PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar tarefa pelo ID (GET /api/task/{id}) > Quando a tarefa solicitada é encontrada > Deve retornar a tarefa encontrada como JSON PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar tarefa pelo ID (GET /api/task/{id}) > Quando a tarefa solicitada é encontrada > Deve retornar as informações do responsável e do finalizador PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar tarefa pelo ID (GET /api/task/{id}) > Quando a tarefa solicitada é encontrada > Deve retornar as informações de auditoria de criação PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar tarefa pelo ID (GET /api/task/{id}) > Quando a tarefa solicitada é encontrada > Deve retornar o ID, título e descrição da tarefa PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar tarefa pelo ID (GET /api/task/{id}) > Quando a tarefa solicitada é encontrada > Deve retornar as informações de auditoria de modificação PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar tarefa pelo ID (GET /api/task/{id}) > Quando a tarefa solicitada é encontrada > Deve retornar o status e a resolução da tarefa PASSED
com.testwithspring.starter.web.TaskRestControllerGetTest > Buscar tarefa pelo ID (GET /api/task/{id}) > Quando a tarefa solicitada é encontrada > Deve retornar a lista de tags da tarefa PASSED
TaskRestControllerGetTest > 16 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.015 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/test-results/test
Generating HTML test report...
Finished generating test html results (0.024 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/reports/tests/test
BUILD SUCCESSFUL in 1s
5 actionable tasks: 1 executed, 4 up-to-date
Resumo
Esta aula nos ensinou três coisas fundamentais:
- Podemos verificar o tipo de conteúdo (
Content-Type) da resposta HTTP usando o método content().contentType(MediaType.APPLICATION_JSON).
- Podemos escrever asserções para documentos JSON retornados pela API usando o método
MockMvcResultMatchers.jsonPath(...), que avalia expressões JsonPath (como $, $[0].title, $.creator.name e $.tags[0].id) combinadas com matchers do Hamcrest.
- Quando escrevemos asserções JsonPath para valores de enums serializados pelo Jackson, devemos comparar o valor do atributo JSON com a representação textual (
enumValue.name) e não com o objeto enum em si.