Após concluir esta aula, você será capaz de escrever testes de integração para funções de repositório que gravam e modificam dados no banco de dados.
Esta aula assume que:
- Você está familiarizado com o DbUnit (e com a anotação
@DatabaseSetup / @ExpectedDatabase).
- Você está familiarizado com o JUnit Jupiter (JUnit 5 / JUnit 6.0.3).
- Você sabe configurar o sistema sob teste para testes de integração de repositório Spring com Kotlin e JUnit Jupiter.
- Você sabe escrever testes de integração para funções de repositório que consultam dados do banco de dados com Kotlin e JUnit Jupiter.
Obtenha o código-fonte no GitHub
Introdução ao Sistema Sob Teste
Durante esta aula, escreveremos testes de integração para a função create() da classe TaskRepository. Essa função recebe como parâmetro um objeto CreateTask, que contém as informações da tarefa a ser criada. Depois de inserir uma nova tarefa no banco de dados, essa função retorna um objeto Task contendo todas as informações da tarefa persistida (incluindo o identificador gerado e os valores padrão de auditoria e status).
O código-fonte do nosso modelo de domínio em Kotlin (2.4.21) é estruturado da seguinte forma:
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
|
data class Creator(val id: Long)
enum class TaskStatus {
CLOSED,
IN_PROGRESS,
OPEN
}
data class CreateTask(
val creator: Creator,
val description: String?,
val status: TaskStatus,
val title: String
)
data class Assignee(val id: Long)
data class Closer(val id: Long)
data class Modifier(val id: Long)
enum class TaskResolution {
DUPLICATE,
DONE,
WONT_DO
}
data class TaskTag(val id: Long, val name: String)
data class Task(
val assignee: Assignee?,
val closer: Closer?,
val creator: Creator,
val description: String?,
val id: Long,
val modifier: Modifier,
val resolution: TaskResolution?,
val status: TaskStatus,
val tags: List<TaskTag>,
val title: String
)
|
A seguir, veremos como configurar nossos testes de integração para suportar operações de escrita e verificação de integridade no banco de dados.
Configurando Nossos Testes de Integração
Podemos configurar nossos testes de integração seguindo estes passos:
Primeiro, precisamos criar uma nova classe de teste de integração e anotá-la com a meta-anotação customizada @DatabaseIntegrationTest (que encapsula @SpringBootTest, o listener do DbUnit e a configuração do perfil integrationTest com Testcontainers 2.0.5 PostgreSQL). Após criar a classe, adicionamos uma constante no companion object para representar o próximo identificador livre gerado pela sequência da tabela.
O código-fonte inicial da nossa classe de teste fica assim:
1
2
3
4
5
6
7
8
9
10
11
|
import com.testwithspring.master.DatabaseIntegrationTest
import org.junit.jupiter.api.DisplayName
@DatabaseIntegrationTest
@DisplayName("Create a new task")
class CreateNewTaskTest {
companion object {
private const val NEXT_FREE_ID = 1L
}
}
|
Segundo, precisamos garantir que possamos injetar instâncias de SoftAssertions diretamente nos parâmetros das funções de teste. Para isso, registramos a extensão SoftAssertionsExtension do AssertJ (3.27.7) com a anotação @ExtendWith.
Após registrar a extensão, nossa classe de teste fica da seguinte forma:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.extension.ExtendWith
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DisplayName("Create a new task")
class CreateNewTaskTest {
companion object {
private const val NEXT_FREE_ID = 1L
}
}
|
Terceiro, precisamos carregar o estado inicial dos dados de teste no banco de dados. Nossos datasets do DbUnit devem garantir que:
- O banco de dados possua dois usuários cadastrados (para que a chave estrangeira do criador da tarefa seja válida).
- O banco de dados não possua tarefas nem tags antes do início do teste.
Configuramos o DbUnit com @DatabaseSetup e garantimos que os arquivos XML sejam processados pela classe ReplacementDataSetLoader (que suporta literais como [null]).
O código-fonte da nossa classe de teste atualizada 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
|
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.extension.ExtendWith
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest {
companion object {
private const val NEXT_FREE_ID = 1L
}
}
|
O dataset DbUnit que insere os usuários no banco de dados (users.xml) possui o seguinte conteúdo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<dataset>
<user_accounts id="1"
email_address="john.doe@gmail.com"
is_enabled="true"
name="John Doe"
password="user"
role="ROLE_USER"/>
<user_accounts id="2"
email_address="anne.admin@gmail.com"
is_enabled="true"
name="Anne Admin"
password="admin"
role="ROLE_ADMIN"/>
</dataset>
|
O dataset DbUnit que assegura que o banco não contém tarefas nem tags (no-tasks-and-tags.xml) possui o seguinte conteúdo:
1
2
3
4
5
|
<dataset>
<tasks/>
<tags/>
<tasks_tags/>
</dataset>
|
Quarto, precisamos declarar duas propriedades privadas somente leitura e injetar suas dependências usando injeção via construtor do Spring:
- A propriedade
jdbcTemplate: contém um objeto NamedParameterJdbcTemplate que utilizaremos para reiniciar a sequência auto-incremento da tabela tasks.
- A propriedade
repository: contém a instância da classe sob teste (TaskRepository).
Com a injeção via construtor declarada, o código-fonte da 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
|
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
}
|
Quinto, adicionamos uma propriedade privada somente leitura chamada idColumnReset, instanciando a classe auxiliar IdColumnReset com o jdbcTemplate. Essa classe utilitária é responsável por reiniciar os valores das sequências das tabelas especificadas.
O código da classe de teste agora é:
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
|
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
}
|
O código-fonte da classe auxiliar IdColumnReset é o seguinte:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
class IdColumnReset internal constructor(
private val jdbcTemplate: NamedParameterJdbcTemplate
) {
companion object {
private const val QUERY_RESET_ID_COLUMN_TEMPLATE =
"ALTER SEQUENCE %s RESTART WITH 1"
}
fun resetIdColumns(vararg tableNames: String) {
tableNames.forEach {
val query = String.format(
QUERY_RESET_ID_COLUMN_TEMPLATE,
it + "_id_seq"
)
jdbcTemplate.update(query, emptyMap<String, Any>())
}
}
}
|
Se você deseja saber mais detalhes sobre a motivação e os impactos dessa abordagem, consulte a aula: Resetting Generated Id Column Values Before Every Test Method.
Sexto, criamos os dados de entrada que serão passados para a função de repositório em nossos testes. Definimos a constante privada INPUT utilizando os valores padronizados de uma classe de constantes de teste (Tasks.WriteLesson).
O código-fonte da 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
|
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
private val INPUT = CreateTask(
creator = Creator(Tasks.WriteLesson.CREATOR_ID),
description = Tasks.WriteLesson.DESCRIPTION,
status = Tasks.WriteLesson.STATUS,
title = Tasks.WriteLesson.TITLE
)
}
|
A classe Tasks é uma classe de constantes que armazena os valores de colunas encontrados nos datasets de teste:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Tasks {
class WriteLesson {
companion object {
const val ID = 2L
const val CREATOR_ID = 1L
const val DESCRIPTION = "This lesson talks about integration testing"
const val TITLE = "Write lesson"
val STATUS = TaskStatus.OPEN
}
}
}
|
Sétimo, escrevemos uma função de configuração anotada com @BeforeEach que é executada antes de cada método de teste. A implementação dessa função garante que a sequência da tabela tasks seja reiniciada para 1 antes de cada execução:
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
|
import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
private val INPUT = CreateTask(
creator = Creator(Tasks.WriteLesson.CREATOR_ID),
description = Tasks.WriteLesson.DESCRIPTION,
status = Tasks.WriteLesson.STATUS,
title = Tasks.WriteLesson.TITLE
)
@BeforeEach
fun resetIdColumn() {
idColumnReset.resetIdColumns("tasks")
}
}
|
Configuramos completamente o sistema sob teste. A seguir, aprenderemos como escrever os testes de integração para validar a inserção e as mutações realizadas no banco de dados.
Escrevendo Testes de Integração para Nossa Função de Repositório
Ao testar operações de escrita em um repositório, precisamos validar dois aspectos essenciais:
- As alterações de estado persistidas no banco de dados (verificadas via
@ExpectedDatabase do DbUnit).
- O objeto de domínio retornado pelo método de repositório (verificado via AssertJ
SoftAssertions).
Podemos implementar esses testes de integração seguindo estes passos:
Primeiro, precisamos garantir que o sistema sob teste utilize o próximo identificador livre ao inserir uma nova tarefa no banco de dados.
Após escrever este primeiro teste, o código-fonte da classe 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
|
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.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
private val INPUT = CreateTask(
creator = Creator(Tasks.WriteLesson.CREATOR_ID),
description = Tasks.WriteLesson.DESCRIPTION,
status = Tasks.WriteLesson.STATUS,
title = Tasks.WriteLesson.TITLE
)
@BeforeEach
fun resetIdColumn() {
idColumnReset.resetIdColumns("tasks")
}
@Test
@DisplayName("Should use the next free id when a new task is saved to the database")
@ExpectedDatabase(
value = "create-task-id-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldUseNextFreeIdWhenNewTaskIsSavedToDatabase() {
repository.create(INPUT)
}
}
|
O dataset esperado do DbUnit (create-task-id-expected.xml), que verifica apenas a atribuição do ID gerado na tabela tasks, possui a seguinte estrutura:
1
2
3
|
<dataset>
<tasks id="1"/>
</dataset>
|
Segundo, devemos verificar se o repositório salva corretamente os valores das colunas de ciclo de vida (auditoria e atribuição: assignee_id, closer_id, creator_id e modifier_id).
Após adicionar a função de teste correspondente, o código-fonte da 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
63
64
65
66
67
68
|
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.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
private val INPUT = CreateTask(
creator = Creator(Tasks.WriteLesson.CREATOR_ID),
description = Tasks.WriteLesson.DESCRIPTION,
status = Tasks.WriteLesson.STATUS,
title = Tasks.WriteLesson.TITLE
)
@BeforeEach
fun resetIdColumn() {
idColumnReset.resetIdColumns("tasks")
}
@Test
@DisplayName("Should use the next free id when a new task is saved to the database")
@ExpectedDatabase(
value = "create-task-id-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldUseNextFreeIdWhenNewTaskIsSavedToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert correct lifecycle column values into the database")
@ExpectedDatabase(
value = "create-task-lifecycle-columns-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectLifecycleColumnValuesIntoDatabase() {
repository.create(INPUT)
}
}
|
O dataset esperado do DbUnit (create-task-lifecycle-columns-expected.xml) valida os valores dessas colunas no registro inserido:
1
2
3
4
5
6
|
<dataset>
<tasks assignee_id="[null]"
closer_id="[null]"
creator_id="1"
modifier_id="1"/>
</dataset>
|
Terceiro, devemos assegurar que o sistema sob teste salve o status e a resolução corretos no banco de dados.
Adicionando o teste de status e resolução, nossa classe 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
|
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.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
private val INPUT = CreateTask(
creator = Creator(Tasks.WriteLesson.CREATOR_ID),
description = Tasks.WriteLesson.DESCRIPTION,
status = Tasks.WriteLesson.STATUS,
title = Tasks.WriteLesson.TITLE
)
@BeforeEach
fun resetIdColumn() {
idColumnReset.resetIdColumns("tasks")
}
@Test
@DisplayName("Should use the next free id when a new task is saved to the database")
@ExpectedDatabase(
value = "create-task-id-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldUseNextFreeIdWhenNewTaskIsSavedToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert correct lifecycle column values into the database")
@ExpectedDatabase(
value = "create-task-lifecycle-columns-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectLifecycleColumnValuesIntoDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should save the correct status and resolution to the database")
@ExpectedDatabase(
value = "create-task-status-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldSaveCorrectStatusAndResolutionToDatabase() {
repository.create(INPUT)
}
}
|
O dataset esperado do DbUnit (create-task-status-expected.xml) possui o seguinte conteúdo:
1
2
3
4
|
<dataset>
<tasks resolution="[null]"
status="OPEN"/>
</dataset>
|
Quarto, devemos verificar se o repositório persiste o título e a descrição informados no objeto de entrada.
Após escrever essa função de teste, a classe passa a ser:
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
|
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.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
private val INPUT = CreateTask(
creator = Creator(Tasks.WriteLesson.CREATOR_ID),
description = Tasks.WriteLesson.DESCRIPTION,
status = Tasks.WriteLesson.STATUS,
title = Tasks.WriteLesson.TITLE
)
@BeforeEach
fun resetIdColumn() {
idColumnReset.resetIdColumns("tasks")
}
@Test
@DisplayName("Should use the next free id when a new task is saved to the database")
@ExpectedDatabase(
value = "create-task-id-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldUseNextFreeIdWhenNewTaskIsSavedToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert correct lifecycle column values into the database")
@ExpectedDatabase(
value = "create-task-lifecycle-columns-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectLifecycleColumnValuesIntoDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should save the correct status and resolution to the database")
@ExpectedDatabase(
value = "create-task-status-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldSaveCorrectStatusAndResolutionToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert the correct title and description to the database")
@ExpectedDatabase(
value = "create-task-title-description-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectTitleAndDescriptionToDatabase() {
repository.create(INPUT)
}
}
|
O dataset esperado do DbUnit (create-task-title-description-expected.xml) valida as colunas textuais correspondentes:
1
2
3
4
|
<dataset>
<tasks description="This lesson talks about integration testing"
title="Write lesson"/>
</dataset>
|
Quinto, devemos verificar que o sistema sob teste cria uma tarefa sem associação a nenhuma tag (as tabelas de relacionamento e de tags devem permanecer vazias).
Após adicionar essa verificação, o código da 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
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
|
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.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
private val INPUT = CreateTask(
creator = Creator(Tasks.WriteLesson.CREATOR_ID),
description = Tasks.WriteLesson.DESCRIPTION,
status = Tasks.WriteLesson.STATUS,
title = Tasks.WriteLesson.TITLE
)
@BeforeEach
fun resetIdColumn() {
idColumnReset.resetIdColumns("tasks")
}
@Test
@DisplayName("Should use the next free id when a new task is saved to the database")
@ExpectedDatabase(
value = "create-task-id-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldUseNextFreeIdWhenNewTaskIsSavedToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert correct lifecycle column values into the database")
@ExpectedDatabase(
value = "create-task-lifecycle-columns-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectLifecycleColumnValuesIntoDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should save the correct status and resolution to the database")
@ExpectedDatabase(
value = "create-task-status-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldSaveCorrectStatusAndResolutionToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert the correct title and description to the database")
@ExpectedDatabase(
value = "create-task-title-description-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectTitleAndDescriptionToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should create a task that has no tags")
@ExpectedDatabase(
value = "create-task-tags-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldCreateTaskThatHasNoTags() {
repository.create(INPUT)
}
}
|
O dataset esperado do DbUnit (create-task-tags-expected.xml) certifica que ambas as tabelas continuam vazias:
1
2
3
4
|
<dataset>
<tags/>
<tasks_tags/>
</dataset>
|
É altamente recomendável utilizar pequenos datasets do DbUnit focados em subconjuntos específicos de colunas ao escrever asserções para o banco de dados. Essa técnica facilita imensamente a identificação da causa raiz quando um teste de integração falha, eliminando mensagens gigantescas de diff contendo dados não relacionados à asserção desejada.
Sexto, precisamos verificar se o método create() retorna corretamente o objeto Task contendo todas as propriedades esperadas da entidade criada. Como queremos verificar múltiplos campos sem interromper o teste na primeira divergência, injetamos um parâmetro do tipo SoftAssertions.
Após adicionar esse teste, nossa classe fica da seguinte forma:
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
|
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.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
private val INPUT = CreateTask(
creator = Creator(Tasks.WriteLesson.CREATOR_ID),
description = Tasks.WriteLesson.DESCRIPTION,
status = Tasks.WriteLesson.STATUS,
title = Tasks.WriteLesson.TITLE
)
@BeforeEach
fun resetIdColumn() {
idColumnReset.resetIdColumns("tasks")
}
@Test
@DisplayName("Should use the next free id when a new task is saved to the database")
@ExpectedDatabase(
value = "create-task-id-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldUseNextFreeIdWhenNewTaskIsSavedToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert correct lifecycle column values into the database")
@ExpectedDatabase(
value = "create-task-lifecycle-columns-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectLifecycleColumnValuesIntoDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should save the correct status and resolution to the database")
@ExpectedDatabase(
value = "create-task-status-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldSaveCorrectStatusAndResolutionToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert the correct title and description to the database")
@ExpectedDatabase(
value = "create-task-title-description-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectTitleAndDescriptionToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should create a task that has no tags")
@ExpectedDatabase(
value = "create-task-tags-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldCreateTaskThatHasNoTags() {
repository.create(INPUT)
}
@Test
@DisplayName("Should return the information of the created task")
fun shouldReturnInformationOfCreatedTask(assertions: SoftAssertions) {
val created = repository.create(INPUT)
assertions.assertThat(created.assignee)
.`as`("assignee")
.isNull()
assertions.assertThat(created.closer)
.`as`("closer")
.isNull()
assertions.assertThat(created.creator.id)
.`as`("creatorId")
.isEqualByComparingTo(Tasks.WriteLesson.CREATOR_ID)
assertions.assertThat(created.description)
.`as`("description")
.isEqualTo(Tasks.WriteLesson.DESCRIPTION)
assertions.assertThat(created.id)
.`as`("id")
.isEqualByComparingTo(NEXT_FREE_ID)
assertions.assertThat(created.modifier.id)
.`as`("modifierId")
.isEqualByComparingTo(Tasks.WriteLesson.CREATOR_ID)
assertions.assertThat(created.resolution)
.`as`("resolution")
.isNull()
assertions.assertThat(created.status)
.`as`("status")
.isEqualTo(Tasks.WriteLesson.STATUS)
assertions.assertThat(created.title)
.`as`("title")
.isEqualTo(Tasks.WriteLesson.TITLE)
}
}
|
A classe SoftAssertions do AssertJ nos permite reduzir a quantidade de funções de teste necessárias sem perder a granularidade do diagnóstico. Diferente das asserções convencionais que interrompem a execução no primeiro erro, o SoftAssertions acumula todas as falhas de asserção e as relata de forma consolidada ao final do teste. Isso encurta significativamente o ciclo de feedback (feedback loop), permitindo que você identifique todos os atributos incorretos em uma única execução.
Sétimo, verificamos se o objeto Task retornado contém uma lista de tags vazia. Para asserções únicas simples, podemos utilizar diretamente o assertThat padrão do AssertJ.
Após escrever este último método de teste, o código-fonte final completo 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
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
|
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.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
value = [
"/com/testwithspring/master/users.xml",
"/com/testwithspring/master/no-tasks-and-tags.xml"
]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Create a new task")
class CreateNewTaskTest(
@Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
@Autowired private val repository: TaskRepository
) {
companion object {
private const val NEXT_FREE_ID = 1L
}
private val idColumnReset = IdColumnReset(jdbcTemplate)
private val INPUT = CreateTask(
creator = Creator(Tasks.WriteLesson.CREATOR_ID),
description = Tasks.WriteLesson.DESCRIPTION,
status = Tasks.WriteLesson.STATUS,
title = Tasks.WriteLesson.TITLE
)
@BeforeEach
fun resetIdColumn() {
idColumnReset.resetIdColumns("tasks")
}
@Test
@DisplayName("Should use the next free id when a new task is saved to the database")
@ExpectedDatabase(
value = "create-task-id-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldUseNextFreeIdWhenNewTaskIsSavedToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert correct lifecycle column values into the database")
@ExpectedDatabase(
value = "create-task-lifecycle-columns-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectLifecycleColumnValuesIntoDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should save the correct status and resolution to the database")
@ExpectedDatabase(
value = "create-task-status-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldSaveCorrectStatusAndResolutionToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should insert the correct title and description to the database")
@ExpectedDatabase(
value = "create-task-title-description-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldInsertCorrectTitleAndDescriptionToDatabase() {
repository.create(INPUT)
}
@Test
@DisplayName("Should create a task that has no tags")
@ExpectedDatabase(
value = "create-task-tags-expected.xml",
assertionMode = DatabaseAssertionMode.NON_STRICT
)
fun shouldCreateTaskThatHasNoTags() {
repository.create(INPUT)
}
@Test
@DisplayName("Should return the information of the created task")
fun shouldReturnInformationOfCreatedTask(assertions: SoftAssertions) {
val created = repository.create(INPUT)
assertions.assertThat(created.assignee)
.`as`("assignee")
.isNull()
assertions.assertThat(created.closer)
.`as`("closer")
.isNull()
assertions.assertThat(created.creator.id)
.`as`("creatorId")
.isEqualByComparingTo(Tasks.WriteLesson.CREATOR_ID)
assertions.assertThat(created.description)
.`as`("description")
.isEqualTo(Tasks.WriteLesson.DESCRIPTION)
assertions.assertThat(created.id)
.`as`("id")
.isEqualByComparingTo(NEXT_FREE_ID)
assertions.assertThat(created.modifier.id)
.`as`("modifierId")
.isEqualByComparingTo(Tasks.WriteLesson.CREATOR_ID)
assertions.assertThat(created.resolution)
.`as`("resolution")
.isNull()
assertions.assertThat(created.status)
.`as`("status")
.isEqualTo(Tasks.WriteLesson.STATUS)
assertions.assertThat(created.title)
.`as`("title")
.isEqualTo(Tasks.WriteLesson.TITLE)
}
@Test
@DisplayName("Should return a task that has no tags")
fun shouldReturnTaskThatHasNoTags() {
val created = repository.create(INPUT)
assertThat(created.tags).isEmpty()
}
}
|
Agora estamos totalmente capacitados para escrever testes de integração expressivos e robustos para funções de repositório que realizam escrita de dados no banco. Vamos sintetizar as principais lições aprendidas.
Resumo
Esta aula nos ensinou duas lições fundamentais para testes de persistência:
- É uma excelente prática adotar pequenos datasets focados do DbUnit ao escrever asserções para dados persistidos no banco de dados com
DatabaseAssertionMode.NON_STRICT. Essa técnica isola as expectativas de colunas específicas (como identificadores gerados, colunas de auditoria ou campos de negócio) e auxilia a identificar a causa raiz imediata de qualquer falha na integração.
- O uso de asserções suaves (soft assertions) com o AssertJ nos permite validar todas as propriedades do objeto retornado pela mutação em um único método de teste, mantendo o relatório consolidado de todas as falhas ocorridas e encurtando expressivamente o ciclo de feedback da equipe de desenvolvimento.