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(
val id: Long,
val title: String,
val status: TaskStatus
)
|
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
|
package com.testwithspring.starter.web
import java.time.Instant
data class TaskDTO(
val id: Long,
val assignee: PersonDTO?,
val closer: PersonDTO?,
val creationTime: Instant,
val creator: PersonDTO,
val description: String,
val modificationTime: Instant,
val modifier: PersonDTO,
val resolution: TaskResolution?,
val status: TaskStatus,
val tags: List<TagDTO>,
val title: String
)
|
Onde PersonDTO e TagDTO são definidos como:
1
2
3
4
5
6
7
8
9
|
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)))
}
}
}
|
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.