Testes de Integração Web: Formulários

Ao concluir esta lição, você será capaz de:

  • Compreender que tipo de massa de dados devemos usar ao escrever testes de integração para métodos de controller que processam submissões de formulários e alteram o banco de dados.
  • Escrever testes de integração completos para métodos de controller que processam formulários tanto no cenário de falha de validação quanto no cenário de sucesso, verificando o estado final das tabelas com datasets focados em um único conceito lógico.

Esta lição assume que:


1. Introdução ao Sistema Sob Teste (SUT)

Precisamos escrever testes de integração para um método de controller que processa requisições POST enviadas para o caminho /task/{taskId}/update. O nome desse método é processUpdateTaskForm(), e ele é implementado seguindo estes quatro passos:

  1. Se o formulário submetido possuir erros de validação (bindingResult.hasErrors()), retornar o nome da view do formulário ("task/update").
  2. Atualizar as informações da tarefa no banco de dados através do TaskCrudService.
  3. Criar uma mensagem de feedback internacionalizada informando que os dados da tarefa foram atualizados e adicioná-la como flash attribute (RedirectAttributes.addFlashAttribute) para ser exibida na próxima página renderizada.
  4. Redirecionar a requisição para a view que exibe os detalhes da tarefa atualizada ("redirect:/task/{taskId}").

O código-fonte do método do controller testado em Kotlin é 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
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
package com.testwithspring.intermediate.web

import com.testwithspring.intermediate.task.TaskCrudService
import com.testwithspring.intermediate.task.TaskDTO
import com.testwithspring.intermediate.task.TaskFormDTO
import jakarta.validation.Valid
import org.springframework.context.MessageSource
import org.springframework.stereotype.Controller
import org.springframework.validation.BindingResult
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.servlet.mvc.support.RedirectAttributes
import java.util.Locale

@Controller
class TaskCrudController(
    private val crudService: TaskCrudService,
    private val messageSource: MessageSource
) {

    @PostMapping("/task/{taskId}/update")
    fun processUpdateTaskForm(
        @Valid @ModelAttribute("task") formObject: TaskFormDTO,
        bindingResult: BindingResult,
        redirectAttributes: RedirectAttributes,
        currentLocale: Locale
    ): String {
        if (bindingResult.hasErrors()) {
            return "task/update"
        }

        val updated: TaskDTO = crudService.update(formObject)
        addFeedbackMessage(
            redirectAttributes,
            "feedback.message.task.updated",
            currentLocale,
            updated.title
        )

        redirectAttributes.addAttribute("taskId", updated.id)
        return "redirect:/task/{taskId}"
    }

    private fun addFeedbackMessage(
        attributes: RedirectAttributes,
        messageCode: String,
        currentLocale: Locale,
        vararg messageParameters: Any
    ) {
        val feedbackMessage = messageSource.getMessage(
            messageCode,
            messageParameters,
            currentLocale
        )
        attributes.addFlashAttribute("feedbackMessage", feedbackMessage)
    }
}

A classe TaskFormDTO é o objeto de formulário utilizado tanto para criar novas tarefas quanto para atualizar tarefas existentes, declarando também as anotações de validação do Jakarta Bean Validation (jakarta.validation.constraints):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.testwithspring.intermediate.task

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size

class TaskFormDTO(
    var id: Long? = null,

    @field:Size(max = 500)
    var description: String? = null,

    @field:NotBlank
    @field:Size(max = 100)
    var title: String? = null
)

Vamos avançar e criar o dataset do DbUnit que inicializa nosso banco de dados em um estado conhecido antes da execução dos testes de integração.


2. Inicializando o Banco de Dados em um Estado Conhecido

Quando escrevemos testes de integração para métodos que modificam os dados do nosso banco de dados, precisamos lembrar de uma característica fundamental do DbUnit: o DbUnit não possui um modo de asserção que ignore a contagem total de linhas (row count) das tabelas declaradas no dataset esperado (@ExpectedDatabase).

Em outras palavras, quando fazemos asserções sobre as linhas de uma tabela do banco de dados, nosso arquivo XML esperado deve especificar todas as linhas presentes naquela tabela ao final do teste.

Como vimos nas boas práticas de datasets, devemos criar datasets pequenos que sejam fáceis de ler, escrever e manter. Por isso, devemos seguir estas duas regras ao testar operações de escrita:

  • Inserção (INSERT): Se estivermos escrevendo testes de integração para um método que insere novas linhas nas tabelas do banco, devemos usar um dataset inicial que não insira nenhuma linha nessas tabelas (empty-database.xml).
  • Atualização (UPDATE): Se estivermos escrevendo testes de integração para um método que atualiza linhas existentes no banco, devemos usar um dataset inicial que insira apenas uma linha nas tabelas atualizadas.

Seguindo essas regras, nossos datasets de verificação (@ExpectedDatabase) permanecem mínimos, pois a tabela modificada pelo código testado terá sempre apenas uma única linha!

Portanto, como estamos testando um método de controller que atualiza as informações de uma tarefa existente, criamos um dataset chamado task.xml (no mesmo pacote da classe de teste) que insere apenas uma linha nas tabelas tasks, tags e tasks_tags:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<dataset>
    <tasks id="2"
           assignee_id="[null]"
           closer_id="[null]"
           creation_time="2016-12-4 11:41:28"
           creator_id="1"
           description="This lesson talks about integration testing"
           modification_time="2016-12-4 11:41:28"
           resolution="[null]"
           status="OPEN"
           title="Write lesson"
           version="0"/>

    <tags id="2"
          creation_time="2016-12-3 11:41:28"
          modification_time="2016-12-3 11:41:28"
          name="lesson"
          version="0"/>

    <tasks_tags task_id="2" tag_id="2"/>
</dataset>

A seguir, escreveremos os testes de integração que verificam o comportamento do método processUpdateTaskForm().


3. Escrevendo os Testes de Integração

Cenário 1: A Validação Falha

Antes de implementarmos os métodos de teste, configuramos nossa classe ProcessUpdateTaskFormWhenValidationFailsTest seguindo estes dois passos:

  1. Configurar o dataset task.xml que popula o estado inicial do banco com @DatabaseSetup("task.xml").
  2. Criar um método privado auxiliar submitEmptyUpdateTaskForm() que submete um formulário de atualização com title e description vazios (""), retornando um ResultActions.

Após configurarmos a classe, precisamos escrever sete testes de integração para o cenário de falha de validação:

  1. Primeiro, verificar que retorna o código de status HTTP 200 OK (status().isOk).
  2. Segundo, garantir que renderiza novamente a view do formulário de edição (view().name("task/update")).
  3. Terceiro, verificar que encaminha o usuário para a URL física da página de edição (forwardedUrl("/WEB-INF/jsp/task/update.jsp")). (Lembre-se: se você usar Thymeleaf/JTE em vez de JSP, este teste de forwardedUrl não é necessário).
  4. Quarto, garantir que exibe um erro de validação para o campo title vazio (model().attributeHasFieldErrorCode("task", "title", equalTo("NotBlank"))).
  5. Quinto, garantir que preserva os valores submetidos dos campos description e title no atributo "task" do Model.
  6. Sexto, verificar que não modifica o parâmetro oculto id (Tasks.WriteLesson.ID) no objeto "task" do Model.
  7. Sétimo, verificar que não atualiza a tarefa no banco de dados, ou seja, garantir que o banco de dados continua contendo exatamente os mesmos dados originais de task.xml usando @ExpectedDatabase(value = "task.xml", assertionMode = DatabaseAssertionMode.NON_STRICT).

O código completo da classe ProcessUpdateTaskFormWhenValidationFailsTest com todos os 7 testes é 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
 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
package com.testwithspring.intermediate.web

import com.github.springtestdbunit.DbUnitTestExecutionListener
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.github.springtestdbunit.annotation.ExpectedDatabase
import com.github.springtestdbunit.assertion.DatabaseAssertionMode
import com.testwithspring.intermediate.IntegrationTest
import com.testwithspring.intermediate.IntegrationTestContext
import com.testwithspring.intermediate.ReplacementDataSetLoader
import com.testwithspring.intermediate.Tasks
import com.testwithspring.intermediate.config.Profiles
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.hasProperty
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestExecutionListeners
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener
import org.springframework.test.context.transaction.TransactionalTestExecutionListener
import org.springframework.test.context.web.ServletTestExecutionListener
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl
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
import org.springframework.web.context.WebApplicationContext

@SpringBootTest
@AutoConfigureMockMvc
@Import(IntegrationTestContext::class)
@TestExecutionListeners(
    listeners = [
        DependencyInjectionTestExecutionListener::class,
        TransactionalTestExecutionListener::class,
        DbUnitTestExecutionListener::class,
        ServletTestExecutionListener::class
    ]
)
@DatabaseSetup("task.xml")
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@IntegrationTest
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class ProcessUpdateTaskFormWhenValidationFailsTest {

    @Autowired
    private lateinit var webAppContext: WebApplicationContext

    private lateinit var mockMvc: MockMvc

    @BeforeEach
    fun configureSystemUnderTest() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
            .build()
    }

    @Test
    fun `deve retornar status HTTP 200 OK`() {
        submitEmptyUpdateTaskForm()
            .andExpect(status().isOk)
    }

    @Test
    fun `deve renderizar a view de atualizacao de tarefa`() {
        submitEmptyUpdateTaskForm()
            .andExpect(view().name("task/update"))
    }

    @Test
    fun `deve encaminhar o usuario para a URL da pagina de atualizacao`() {
        submitEmptyUpdateTaskForm()
            .andExpect(forwardedUrl("/WEB-INF/jsp/task/update.jsp"))
    }

    @Test
    fun `deve exibir erro de validacao sobre titulo vazio`() {
        submitEmptyUpdateTaskForm()
            .andExpect(
                model().attributeHasFieldErrorCode(
                    "task",
                    "title",
                    equalTo("NotBlank")
                )
            )
    }

    @Test
    fun `deve exibir os valores dos campos submetidos no formulario`() {
        submitEmptyUpdateTaskForm()
            .andExpect(
                model().attribute(
                    "task",
                    allOf(
                        hasProperty("description", equalTo("")),
                        hasProperty("title", equalTo(""))
                    )
                )
            )
    }

    @Test
    fun `nao deve modificar o parametro oculto id`() {
        submitEmptyUpdateTaskForm()
            .andExpect(
                model().attribute(
                    "task",
                    hasProperty("id", equalTo(Tasks.WriteLesson.ID))
                )
            )
    }

    @Test
    @ExpectedDatabase(
        value = "task.xml",
        assertionMode = DatabaseAssertionMode.NON_STRICT
    )
    fun `nao deve atualizar a tarefa no banco de dados`() {
        submitEmptyUpdateTaskForm()
    }

    private fun submitEmptyUpdateTaskForm(): ResultActions {
        return mockMvc.perform(
            post("/task/{taskId}/update", Tasks.WriteLesson.ID)
                .param("description", "")
                .param("id", Tasks.WriteLesson.ID.toString())
                .param("title", "")
        )
    }
}

A seguir, escreveremos os testes de integração que garantem que o método do controller funciona conforme o esperado quando a validação é bem-sucedida.


Cenário 2: A Validação é Bem-Sucedida

Antes de escrevermos os testes, configuramos nossa classe ProcessUpdateTaskFormWhenValidationIsSuccessfulTest seguindo estes quatro passos:

  1. Configurar o dataset inicial @DatabaseSetup("task.xml") na classe.
  2. Declarar a constante FEEDBACK_MESSAGE_TASK_UPDATED contendo a mensagem de feedback esperada lida do arquivo de localização (messages.properties).
  3. Declarar as constantes NEW_DESCRIPTION e NEW_TITLE contendo a nova descrição e o novo título da tarefa atualizada.
  4. Criar o método privado auxiliar submitUpdateTaskForm() que submete o formulário de atualização com os novos valores.

Três pontos importantes antes de prosseguirmos:

  • Declaramos FEEDBACK_MESSAGE_TASK_UPDATED, NEW_DESCRIPTION e NEW_TITLE dentro da própria classe de teste porque elas são utilizadas exclusivamente por esta classe.
  • Se você estiver escrevendo testes de integração para um método de controller que insere uma nova tarefa no banco de dados (ex: processCreateNewTaskForm()), lembre-se de resetar os valores gerados da coluna id no método @BeforeEach antes de cada teste!
  • Na classe IntegrationTestContext (com o profile integrationTest), configuramos um bean ConstantDateTimeService que retorna sempre o instante fixo "2016-12-03 21:14:28.0" ao atualizar entidades auditadas, permitindo fazer asserções exatas sobre modification_time.

Após configurarmos a classe, escrevemos nove testes de integração para o método processUpdateTaskForm():

Primeiro, verificamos que retorna o código de status HTTP 302 Found (status().isFound).

Segundo, garantimos que redireciona o usuário para a view de visualização da tarefa (view().name("redirect:/task/{taskId}") com o atributo de modelo taskId = "2").

Terceiro, garantimos que redireciona o usuário para a URL final /task/2 (redirectedUrl("/task/2")).

Quarto, garantimos que adiciona a mensagem de feedback como flash attribute (flash().attribute("feedbackMessage", FEEDBACK_MESSAGE_TASK_UPDATED)).

Quinto, verificamos que atualiza o título e a descrição da tarefa encontrada no banco de dados usando o dataset update-task-should-update-title-and-description.xml:

1
2
3
4
5
<dataset>
    <tasks id="2"
           description="The old lesson was not good"
           title="Rewrite an existing lesson"/>
</dataset>

Sexto, verificamos que atualiza os campos de ciclo de vida (modification_time e version de 0 para 1, preservando creation_time e creator_id) usando o dataset update-task-should-update-lifecycle-fields.xml:

1
2
3
4
5
6
7
8
<dataset>
    <!-- Apenas modification_time e version devem ser atualizados -->
    <tasks id="2"
           creation_time="2016-12-4 11:41:28"
           creator_id="1"
           modification_time="2016-12-03 21:14:28.0"
           version="1"/>
</dataset>

Sétimo, garantimos que não altera o status (nem closer_id ou resolution) da tarefa atualizada usando o dataset update-task-should-not-change-status.xml:

1
2
3
4
5
6
<dataset>
    <tasks id="2"
           closer_id="[null]"
           resolution="[null]"
           status="OPEN"/>
</dataset>

Oitavo, verificamos que não altera o responsável (assignee_id) da tarefa atualizada usando o dataset update-task-should-not-change-assignee.xml:

1
2
3
4
<dataset>
    <tasks id="2"
           assignee_id="[null]"/>
</dataset>

Nono, verificamos que não faz nenhuma alteração nas tags da tarefa atualizada usando o dataset update-task-should-not-change-tags.xml:

1
2
3
4
5
6
7
8
9
<dataset>
    <tags id="2"
          creation_time="2016-12-3 11:41:28"
          modification_time="2016-12-3 11:41:28"
          name="lesson"
          version="0"/>

    <tasks_tags task_id="2" tag_id="2"/>
</dataset>

O código completo da classe ProcessUpdateTaskFormWhenValidationIsSuccessfulTest reunindo todos os 9 testes é 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
 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
package com.testwithspring.intermediate.web

import com.github.springtestdbunit.DbUnitTestExecutionListener
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.github.springtestdbunit.annotation.ExpectedDatabase
import com.github.springtestdbunit.assertion.DatabaseAssertionMode
import com.testwithspring.intermediate.IntegrationTest
import com.testwithspring.intermediate.IntegrationTestContext
import com.testwithspring.intermediate.ReplacementDataSetLoader
import com.testwithspring.intermediate.Tasks
import com.testwithspring.intermediate.config.Profiles
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestExecutionListeners
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener
import org.springframework.test.context.transaction.TransactionalTestExecutionListener
import org.springframework.test.context.web.ServletTestExecutionListener
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.flash
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.model
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl
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
import org.springframework.web.context.WebApplicationContext

@SpringBootTest
@AutoConfigureMockMvc
@Import(IntegrationTestContext::class)
@TestExecutionListeners(
    listeners = [
        DependencyInjectionTestExecutionListener::class,
        TransactionalTestExecutionListener::class,
        DbUnitTestExecutionListener::class,
        ServletTestExecutionListener::class
    ]
)
@DatabaseSetup("task.xml")
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@IntegrationTest
@ActiveProfiles(Profiles.INTEGRATION_TEST)
class ProcessUpdateTaskFormWhenValidationIsSuccessfulTest {

    companion object {
        private const val FEEDBACK_MESSAGE_TASK_UPDATED =
            "The information of a task was updated successfully."
        private const val NEW_DESCRIPTION = "The old lesson was not good"
        private const val NEW_TITLE = "Rewrite an existing lesson"
    }

    @Autowired
    private lateinit var webAppContext: WebApplicationContext

    private lateinit var mockMvc: MockMvc

    @BeforeEach
    fun configureSystemUnderTest() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
            .build()
    }

    @Test
    fun `deve retornar status HTTP 302 Found`() {
        submitUpdateTaskForm()
            .andExpect(status().isFound)
    }

    @Test
    fun `deve redirecionar o usuario para a view de visualizacao da tarefa`() {
        submitUpdateTaskForm()
            .andExpect(view().name("redirect:/task/{taskId}"))
            .andExpect(
                model().attribute(
                    "taskId",
                    equalTo(Tasks.WriteLesson.ID.toString())
                )
            )
    }

    @Test
    fun `deve redirecionar o usuario para a URL da pagina de visualizacao da tarefa`() {
        submitUpdateTaskForm()
            .andExpect(redirectedUrl("/task/2"))
    }

    @Test
    fun `deve adicionar mensagem de feedback como flash attribute`() {
        submitUpdateTaskForm()
            .andExpect(
                flash().attribute(
                    "feedbackMessage",
                    FEEDBACK_MESSAGE_TASK_UPDATED
                )
            )
    }

    @Test
    @ExpectedDatabase(
        value = "update-task-should-update-title-and-description.xml",
        assertionMode = DatabaseAssertionMode.NON_STRICT
    )
    fun `deve atualizar o titulo e a descricao da tarefa`() {
        submitUpdateTaskForm()
    }

    @Test
    @ExpectedDatabase(
        value = "update-task-should-update-lifecycle-fields.xml",
        assertionMode = DatabaseAssertionMode.NON_STRICT
    )
    fun `deve atualizar modificationTime e version da tarefa`() {
        submitUpdateTaskForm()
    }

    @Test
    @ExpectedDatabase(
        value = "update-task-should-not-change-status.xml",
        assertionMode = DatabaseAssertionMode.NON_STRICT
    )
    fun `nao deve alterar o status da tarefa`() {
        submitUpdateTaskForm()
    }

    @Test
    @ExpectedDatabase(
        value = "update-task-should-not-change-assignee.xml",
        assertionMode = DatabaseAssertionMode.NON_STRICT
    )
    fun `nao deve alterar o responsavel (assignee) da tarefa`() {
        submitUpdateTaskForm()
    }

    @Test
    @ExpectedDatabase(
        value = "update-task-should-not-change-tags.xml",
        assertionMode = DatabaseAssertionMode.NON_STRICT
    )
    fun `nao deve fazer nenhuma alteracao nas tags da tarefa atualizada`() {
        submitUpdateTaskForm()
    }

    private fun submitUpdateTaskForm(): ResultActions {
        return mockMvc.perform(
            post("/task/{taskId}/update", Tasks.WriteLesson.ID)
                .param("description", NEW_DESCRIPTION)
                .param("id", Tasks.WriteLesson.ID.toString())
                .param("title", NEW_TITLE)
        )
    }
}

Por que dividimos a verificação de banco em 5 datasets pequenos?

Como você pode notar, a ideia por trás dos últimos cinco testes de integração é escrever nossas asserções usando datasets do DbUnit que validam um único conceito lógico por vez (título/descrição, campos de auditoria/versão, status/resolução, responsável e tags). Esses pequenos datasets de 3 a 8 linhas são extremamente fáceis de ler e deixam explícito na falha do teste qual regra de negócio foi violada!

Vamos resumir o que aprendemos nesta lição.


Resumo

Esta lição nos ensinou quatro pontos essenciais:

  • O DbUnit não possui um modo de asserção que ignore a quantidade de linhas (row count) das tabelas declaradas no dataset esperado.
  • Ao escrever testes de integração para um método que insere novas linhas nas tabelas do banco, devemos usar um dataset inicial que não insira nenhuma linha nessas tabelas.
  • Ao escrever testes de integração para um método que atualiza linhas existentes no banco, devemos usar um dataset inicial que insira apenas uma linha nas tabelas atualizadas.
  • Ao escrever asserções para os dados persistidos no banco, devemos usar datasets pequenos e focados que validem um único conceito lógico por teste.