Testando Tipos Customizados no PostgreSQL: Fundamentos

Ao finalizarmos esta aula, seremos capazes de:

  • Saber como implementar um tipo de dado customizado no DbUnit.
  • Compreender como garantir que o DbUnit utilize nosso tipo de dado customizado em testes de integração.

Esta aula assume que:

  • Você já está familiarizado com o DbUnit (gerenciamento e carregamento de datasets de teste via @DatabaseSetup).
  • Você sabe configurar testes de integração que utilizam o ecossistema Spring Boot com Kotlin 2.4.21, Spring Boot 4.1.2, Gradle 9.8.2 e JUnit Jupiter 6.0.3.
  • Você sabe como executar bancos de dados PostgreSQL em contêineres de teste utilizando Testcontainers 2.0.5.

Obter o código-fonte no GitHub

Implementando um Tipo de Dado Customizado no DbUnit

Quando queremos implementar um tipo de dado customizado no DbUnit, precisamos criar uma nova classe de tipo de dados que estende a classe AbstractDataType.

Depois de criarmos essa classe, seu esqueleto inicial de código-fonte fica da seguinte forma:

1
2
3
4
5
6
7
package com.testwithspring.master.dbunitdatatypes

import org.dbunit.dataset.datatype.AbstractDataType

class CustomDbUnitDataType : AbstractDataType {

}

Podemos agora implementar nosso tipo de dado customizado no DbUnit seguindo estes passos:

Primeiro, precisamos implementar um construtor que invoque o construtor da classe AbstractDataType. Ao invocar o construtor da superclasse AbstractDataType, devemos fornecer quatro argumentos:

  • O nome do tipo de dado SQL (name: String) suportado pelo nosso tipo de dados do DbUnit.
  • O código numérico do tipo de dado SQL (sqlType: Int) suportado pelo nosso tipo de dados do DbUnit (como as constantes padronizadas em java.sql.Types).
  • A classe do tipo de objeto (classType: Class<*>) que contém o valor inserido na coluna da tabela do banco de dados.
  • Um valor booleano (isNumber: Boolean) que especifica se nosso tipo de dados do DbUnit suporta valores numéricos.

Depois de implementarmos o construtor obrigatório, o código-fonte da nossa classe de tipo de dados em Kotlin 2.4.21 fica da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.testwithspring.master.dbunitdatatypes

import org.dbunit.dataset.datatype.AbstractDataType

class CustomDbUnitDataType(
    name: String,
    sqlType: Int,
    classType: Class<*>,
    isNumber: Boolean
) : AbstractDataType(name, sqlType, classType, isNumber) {

}

Para fins de referência e comparação com o ecossistema Java clássico, a implementação equivalente em Java seria:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.testwithspring.master.dbunitdatatypes;

import org.dbunit.dataset.datatype.AbstractDataType;

public class CustomDbUnitDataType extends AbstractDataType {

    public CustomDbUnitDataType(String name,
                                int sqlType,
                                Class classType,
                                boolean isNumber) {
        super(name, sqlType, classType, isNumber);
    }
}

Segundo, precisamos implementar o método typeCast() da classe DataType (definido na interface DataType e herdado via AbstractDataType). Esse método transforma o objeto fornecido como parâmetro no método em um objeto que será inserido em uma coluna de uma tabela do banco de dados.

Depois de implementarmos esse método, o código-fonte da nossa classe de tipo de dados fica da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.testwithspring.master.dbunitdatatypes

import org.dbunit.dataset.datatype.AbstractDataType
import org.dbunit.dataset.datatype.TypeCastException

class CustomDbUnitDataType(
    name: String,
    sqlType: Int,
    classType: Class<*>,
    isNumber: Boolean
) : AbstractDataType(name, sqlType, classType, isNumber) {

    @Throws(TypeCastException::class)
    override fun typeCast(value: Any?): Any? {
        return null
    }
}

Nota:

Este tipo de dados é inútil neste momento porque não faz nada (retorna apenas null). Se você quiser saber como implementar tipos de dados customizados reais e funcionais no DbUnit (como suporte a colunas de array TEXT[] ou colunas JSONB no PostgreSQL), confira as próximas aulas deste capítulo.

A seguir, descobriremos como podemos garantir que o DbUnit utilize nosso tipo de dado customizado.

Alterando a Configuração do DbUnit

Quando queremos garantir que o DbUnit utilize nosso tipo de dado customizado, precisamos seguir estes passos:

Primeiro, precisamos estender a classe factory de tipos de dados (data type factory) que assegura que o DbUnit consiga manipular os tipos de dados do banco de dados utilizado. Como nossa aplicação utiliza o banco de dados PostgreSQL (gerenciado em tempo de teste via Testcontainers 2.0.5), devemos estender a classe PostgresqlDataTypeFactory.

Depois de criarmos nossa classe factory de tipos de dados, devemos sobrescrever o método createDataType() da interface IDataTypeFactory e implementá-lo seguindo estes passos:

  1. Se o sqlType (ou o nome sqlTypeName) for igual ao tipo de dados suportado pelo nosso tipo customizado do DbUnit, retornamos uma nova instância de CustomDbUnitDataType.
  2. Caso contrário, invocamos o método createDataType() da superclasse (super.createDataType(...)). Isso garante que o DbUnit continue manipulando normalmente todos os tipos de dados padrão (default) do banco de dados utilizado.

O código-fonte da nossa classe factory de tipos de dados em Kotlin 2.4.21 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
package com.testwithspring.master.dbunitdatatypes

import org.dbunit.dataset.datatype.DataType
import org.dbunit.dataset.datatype.DataTypeException
import org.dbunit.ext.postgresql.PostgresqlDataTypeFactory

class CustomPostgreSQLDataTypeFactory : PostgresqlDataTypeFactory() {

    @Throws(DataTypeException::class)
    override fun createDataType(
        sqlType: Int,
        sqlTypeName: String?,
        tableName: String?,
        columnName: String?
    ): DataType {
        // Configure o tipo de dado customizado aqui
        // (Nas próximas aulas, verificaremos se sqlTypeName corresponde
        // a "text[]", "_text" ou "jsonb", retornando nosso tipo customizado).

        return super.createDataType(
            sqlType,
            sqlTypeName,
            tableName,
            columnName
        )
    }
}

Segundo, precisamos criar uma nova classe de configuração do Spring e anotá-la com a anotação @Configuration. Depois de criarmos essa classe, precisamos implementá-la seguindo estes passos:

  1. Criar um bean DatabaseConfigBean que garante que o DbUnit utilize nossa factory customizada de tipos de dados (CustomPostgreSQLDataTypeFactory).
  2. Assegurar que o DbUnit utilize a configuração customizada fornecida pelo bean DatabaseConfigBean, registrando um bean do tipo DatabaseDataSourceConnectionFactoryBean.

Depois de escrevermos nossa classe de configuração, seu código-fonte em Kotlin 2.4.21 com Spring Boot 4.1.2 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
package com.testwithspring.master.dbunitdatatypes

import com.github.springtestdbunit.bean.DatabaseConfigBean
import com.github.springtestdbunit.bean.DatabaseDataSourceConnectionFactoryBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import javax.sql.DataSource

@Configuration
class TestDatabaseConfiguration {

    @Bean
    fun dbUnitDatabaseConfig(): DatabaseConfigBean {
        val config = DatabaseConfigBean()
        config.datatypeFactory = CustomPostgreSQLDataTypeFactory()
        return config
    }

    @Bean
    fun dbUnitDatabaseConnection(
        dataSource: DataSource,
        dbConfig: DatabaseConfigBean
    ): DatabaseDataSourceConnectionFactoryBean {
        val connectionFactory = DatabaseDataSourceConnectionFactoryBean()
        connectionFactory.setDataSource(dataSource)
        connectionFactory.setDatabaseConfig(dbConfig)
        return connectionFactory
    }
}

Entendendo como esses dois beans colaboram com o spring-test-dbunit:

  • DatabaseConfigBean: funciona como uma ponte declarativa para o objeto DatabaseConfig do DbUnit. Ao chamar config.datatypeFactory = CustomPostgreSQLDataTypeFactory(), definimos a propriedade interna PROPERTY_DATATYPE_FACTORY do DbUnit, instruindo-o a consultar nossa factory ao interpretar colunas do PostgreSQL.
  • DatabaseDataSourceConnectionFactoryBean: a integração do spring-test-dbunit procura um bean que forneça instâncias de IDatabaseConnection. Esse bean factory recebe o DataSource autoconfigurado pelo Spring Boot (que aponta para o banco de dados PostgreSQL do Testcontainers) e injeta a nossa instância de DatabaseConfigBean.

Antes de podermos utilizar nosso tipo de dado customizado do DbUnit em nossos datasets de teste, precisamos garantir que nossos testes de integração carreguem essa nossa classe de configuração customizada. Vamos descobrir como fazer isso.

Configurando Nossos Testes de Integração

Quando usamos a anotação @SpringBootTest, precisamos garantir que nossos testes de integração carreguem o contexto de aplicação (ApplicationContext) da nossa aplicação Spring Boot e adicionem nossos beans customizados do Spring ao contexto carregado.

Para evitar repetição de código em cada classe de teste, criamos uma anotação customizada de teste (meta-anotação composta) denominada @DatabaseIntegrationTest.

O código-fonte da anotação que configura nossos testes de integração em Kotlin 2.4.21 e JUnit Jupiter 6.0.3 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
package com.testwithspring.master.dbunitdatatypes

import com.github.springtestdbunit.DbUnitTestExecutionListener
import org.junit.jupiter.api.Tag
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.TestExecutionListeners
import java.lang.annotation.Inherited

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Inherited
@SpringBootTest(
    classes = [
        CustomDbUnitDataTypeApplication::class,
        TestDatabaseConfiguration::class
    ]
)
@TestExecutionListeners(
    listeners = [DbUnitTestExecutionListener::class],
    mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
@ActiveProfiles("integrationTest")
@Tag("integrationTest")
annotation class DatabaseIntegrationTest

Analisando a estrutura da nossa anotação:

  1. @SpringBootTest(classes = [CustomDbUnitDataTypeApplication::class, TestDatabaseConfiguration::class]): instrui o Spring Boot a carregar a classe principal da aplicação e a registrar explicitamente nossa classe TestDatabaseConfiguration. Dessa forma, os beans dbUnitDatabaseConfig e dbUnitDatabaseConnection tornam-se ativos no contexto de teste.
  2. @TestExecutionListeners: registra o listener DbUnitTestExecutionListener do spring-test-dbunit com o modo de mesclagem MERGE_WITH_DEFAULTS. Esse listener é responsável por interceptar o ciclo de vida dos testes do JUnit Jupiter e acionar os mecanismos de carga do DbUnit (como @DatabaseSetup, @ExpectedDatabase e @DatabaseTearDown).
  3. @ActiveProfiles("integrationTest"): ativa o perfil de testes de integração, que carrega as propriedades de banco de dados do Testcontainers (spring.datasource.url=jdbc:tc:postgresql:17-alpine:///integrationTest).
  4. @Tag("integrationTest"): categoriza o teste no JUnit Jupiter 6.0.3, possibilitando que a suíte seja executada de maneira filtrada ou dedicada no Gradle 9.8.2 (./gradlew test -PincludeTags=integrationTest).

Com essa meta-anotação configurada, qualquer classe de teste de integração que precise interagir com o PostgreSQL e carregar datasets com suporte a tipos customizados necessita apenas ser anotada com @DatabaseIntegrationTest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.testwithspring.master.dbunitdatatypes

import com.github.springtestdbunit.annotation.DatabaseSetup
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate

@DatabaseIntegrationTest
@DisplayName("Testes de integração com tipos customizados do DbUnit no PostgreSQL")
class CustomDataTypeIntegrationTest(
    @Autowired private val jdbcTemplate: NamedParameterJdbcTemplate
) {

    @Test
    @DatabaseSetup("/com/testwithspring/master/custom-datatype-dataset.xml")
    fun `deve carregar dataset com sucesso utilizando factory customizada`() {
        // O DbUnit agora invoca nossa CustomPostgreSQLDataTypeFactory
        // para converter os valores do dataset XML para os tipos nativos do banco de dados
    }
}

Agora somos capazes de implementar um tipo de dado customizado no DbUnit e garantir que o DbUnit utilize nosso tipo customizado durante a execução dos testes. Vamos resumir o que aprendemos nesta aula.

Resumo

Esta aula nos ensinou quatro pontos fundamentais:

  • Quando queremos implementar um tipo de dado customizado no DbUnit, precisamos estender a classe AbstractDataType e implementar o método typeCast().
  • Uma factory de tipos de dados específica do banco de dados (como PostgresqlDataTypeFactory) garante que o DbUnit consiga manipular os tipos de dados nativos do banco de dados utilizado.
  • Se quisermos adicionar suporte a novos tipos de dados no DbUnit, precisamos estender a classe factory de tipos de dados correta e sobrescrever o método createDataType().
  • Quando queremos utilizar nosso tipo de dado customizado em nossos datasets do DbUnit, devemos garantir que o DbUnit utilize nossa factory de tipos de dados configurando os beans DatabaseConfigBean e DatabaseDataSourceConnectionFactoryBean no contexto do Spring Boot.

Obter o código-fonte no GitHub