Testes Unitários de Controladores Web: Renderização de Dados

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:

  1. 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.
  2. 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:

  1. Busca todas as tarefas no banco de dados utilizando o método findAll() da interface TaskCrudService.
  2. Coloca as tarefas encontradas em um atributo do Model chamado "tasks".
  3. 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:

TaskListDTO.kt
Arquivo Real KOTLIN
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
)

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:

  1. Adicionamos duas propriedades TaskListDTO (first e second) à classe WhenTwoTasksAreFound.
  2. Adicionamos um método fábrica privado chamado createTask() dentro de WhenTwoTasksAreFound que cria e retorna um objeto TaskListDTO.
  3. Adicionamos um método @BeforeEach fun returnTwoTasks() à classe WhenTwoTasksAreFound.
  4. 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():

  1. Verificamos que ele retorna o código de status HTTP 200 OK (status().isOk).
  2. Garantimos que ele renderiza a view de listagem de tarefas (view().name("task/list")).
  3. Verificamos que a lista de tarefas no atributo "tasks" do Model contém exatamente duas tarefas (hasSize(2)).
  4. Garantimos que as duas tarefas estão na ordem exata retornada pelo serviço (contains(first, second)).
  5. 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!


Teste 2: Renderizando as Informações de uma Única Tarefa (showTask)

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:

  1. 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")).
  2. Coloca a tarefa encontrada em um atributo do Model chamado "task".
  3. 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 as informações detalhadas de uma única tarefa:

TaskDTO.kt
Arquivo Real KOTLIN
 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()
)

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
        }
    }

Passo 2: Configurar o cenário com TaskDTOBuilder

Segundo, configuramos nossos casos de teste dentro de WhenTaskIsFound seguindo estes passos:

  1. Adicionamos um método @BeforeEach fun returnFoundTask() à classe WhenTaskIsFound.
  2. 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).
  3. 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)):

  1. Verificamos que o código de status HTTP 200 OK é retornado.
  2. Garantimos que o método showTask() renderiza a view "task/view".
  3. Verificamos que o atributo "task" do Model contém todas as propriedades esperadas da tarefa encontrada.

O código-fonte completo da classe TaskCrudControllerRenderTest em nosso repositório real de testes fica assim:

TaskCrudControllerRenderTest.kt
Arquivo Real KOTLIN
  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
package com.testwithspring.starter.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.hamcrest.Matchers.nullValue
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.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")
@DisplayName("01-07-04: Testes Unitários de Controladores Web - Renderização de Dados")
class TaskCrudControllerRenderTest {

    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
    @DisplayName("Renderizar todas as tarefas (GET /)")
    inner class ShowTaskList {

        @Nested
        @DisplayName("Quando duas tarefas são encontradas")
        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
            @DisplayName("Deve retornar o código de status HTTP 200 OK")
            fun shouldReturnHttpStatusCodeOk() {
                mockMvc.perform(get("/"))
                    .andExpect(status().isOk)
            }

            @Test
            @DisplayName("Deve renderizar a view task/list")
            fun shouldRenderTaskListView() {
                mockMvc.perform(get("/"))
                    .andExpect(view().name("task/list"))
            }

            @Test
            @DisplayName("Deve exibir lista que contém exatamente duas tarefas")
            fun shouldShowTaskListThatHasTwoTasks() {
                mockMvc.perform(get("/"))
                    .andExpect(model().attribute("tasks", hasSize<Any>(2)))
            }

            @Test
            @DisplayName("Deve exibir as duas tarefas na ordem correta")
            fun shouldShowTwoTasksInCorrectOrder() {
                mockMvc.perform(get("/"))
                    .andExpect(model().attribute("tasks", contains(first, second)))
            }

            @Test
            @DisplayName("Deve exibir informações corretas das tarefas no Model")
            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))
                                    )
                                )
                            )
                        )
                    )
            }
        }
    }

    @Nested
    @DisplayName("Renderizar detalhes de uma tarefa (GET /task/{taskId})")
    inner class ShowTask {

        @Nested
        @DisplayName("Quando a tarefa é encontrada")
        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
            @DisplayName("Deve retornar o código de status HTTP 200 OK")
            fun shouldReturnHttpStatusCodeOk() {
                mockMvc.perform(get("/task/{taskId}", 1L))
                    .andExpect(status().isOk)
            }

            @Test
            @DisplayName("Deve renderizar a view task/view")
            fun shouldRenderViewTaskView() {
                mockMvc.perform(get("/task/{taskId}", 1L))
                    .andExpect(view().name("task/view"))
            }

            @Test
            @DisplayName("Deve exibir a tarefa encontrada com suas propriedades no Model")
            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())
                            )
                        )
                    )
            }
        }
    }
}

Executando Nossos Testes de Renderização no Gradle

Ao executarmos a suíte TaskCrudControllerRenderTest através do Gradle (./gradlew test --tests TaskCrudControllerRenderTest -i), inspecionamos o log de validação com sucesso:

Evidência Real de Execução: ./gradlew test --tests TaskCrudControllerRenderTest -i
Terminal Log
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.TaskCrudControllerRenderTest > Renderizar todas as tarefas (GET /) > Quando duas tarefas são encontradas > Deve retornar o código de status HTTP 200 OK PASSED
com.testwithspring.starter.web.TaskCrudControllerRenderTest > Renderizar todas as tarefas (GET /) > Quando duas tarefas são encontradas > Deve renderizar a view task/list PASSED
com.testwithspring.starter.web.TaskCrudControllerRenderTest > Renderizar todas as tarefas (GET /) > Quando duas tarefas são encontradas > Deve exibir lista que contém exatamente duas tarefas PASSED
com.testwithspring.starter.web.TaskCrudControllerRenderTest > Renderizar todas as tarefas (GET /) > Quando duas tarefas são encontradas > Deve exibir as duas tarefas na ordem correta PASSED
com.testwithspring.starter.web.TaskCrudControllerRenderTest > Renderizar todas as tarefas (GET /) > Quando duas tarefas são encontradas > Deve exibir informações corretas das tarefas no Model PASSED
com.testwithspring.starter.web.TaskCrudControllerRenderTest > Renderizar detalhes de uma tarefa (GET /task/{taskId}) > Quando a tarefa é encontrada > Deve retornar o código de status HTTP 200 OK PASSED
com.testwithspring.starter.web.TaskCrudControllerRenderTest > Renderizar detalhes de uma tarefa (GET /task/{taskId}) > Quando a tarefa é encontrada > Deve renderizar a view task/view PASSED
com.testwithspring.starter.web.TaskCrudControllerRenderTest > Renderizar detalhes de uma tarefa (GET /task/{taskId}) > Quando a tarefa é encontrada > Deve exibir a tarefa encontrada com suas propriedades no Model PASSED

TaskCrudControllerRenderTest > 8 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.010 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.018 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

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).