Testando Colunas JSONB do PostgreSQL com Jackson e Kotlin
Ao finalizarmos esta aula, seremos capazes de:
- Escrever um tipo de dado customizado no DbUnit que forneça suporte integral aos tipos de dados
jsonejsonbdo banco de dados PostgreSQL. - Garantir que o DbUnit utilize a nossa fábrica de tipos de dados customizada durante o carregamento de datasets.
- Mapear e testar entidades de domínio em Kotlin integradas ao Jackson Kotlin Module para persistência e consulta de documentos JSONB no Spring Boot 4.1.2.
Esta aula assume que:
- Você está familiarizado com o DbUnit e seu mecanismo de gerenciamento de datasets em testes de integração.
- Você sabe configurar testes de integração utilizando o ecossistema Spring Boot com banco de dados real.
- Você compreende a implementação básica de tipos de dados customizados no DbUnit (conforme abordado na aula de fundamentos desta série).
1. Configuração do Projeto e Dependências (Gradle Kotlin DSL)
Para integrar testes de banco de dados com tipos nativos json/jsonb do PostgreSQL, precisamos do driver JDBC do PostgreSQL, do módulo do Jackson para Kotlin, do DbUnit e das bibliotecas de teste modernas configuradas no build.gradle.kts:
|
|
2. Criando Nosso Conjunto de Dados do DbUnit (DbUnit Data Set)
Precisamos escrever testes de integração para um repositório que realiza a leitura e manipulação de informações a partir da tabela products. Quando inicializamos o banco de dados em um estado prévio conhecido, precisamos definir o valor da coluna reviews, a qual armazena um documento JSON contendo a lista de avaliações do produto.
Quando configuramos o valor dessa coluna em nosso dataset XML do DbUnit, a abordagem mais direta e recomendada é utilizar a representação textual (String) do documento JSON. Como o XML utiliza aspas duplas para delimitar o valor dos atributos, devemos escapar as aspas internas do JSON através da entidade ".
Após criarmos nosso dataset do DbUnit (products.xml), o arquivo fica estruturado da seguinte forma:
|
|
Por que o DbUnit precisa de um tipo customizado para JSON?
No PostgreSQL, colunas do tipojsonoujsonbnão são mapeadas por padrão para tipos JDBC convencionais comoVARCHAR. O driver JDBC as identifica com o código genéricoTypes.OTHER(1111) ousqlTypeName = "json"/"jsonb". Sem um tipo específico registrado, o DbUnit tenta convertê-las para tipos incompatíveis ou lança umaTypeCastExceptiondurante o carregamento dos registros.
A seguir, veremos como criar o tipo de dado customizado no DbUnit que gerencia essa conversão.
3. Escrevendo Nosso Tipo de Dado Customizado no DbUnit
Para criar um tipo de dado customizado no DbUnit, precisamos criar uma nova classe que estenda AbstractDataType e implementar o método typeCast() da classe DataType.
Podemos implementar o método typeCast() seguindo estes passos:
- Se o valor fornecido como parâmetro do método for um objeto
String, retornamos o próprio valor fornecido. - Se o valor fornecido como parâmetro do método não for um objeto
String, lançamos uma nova exceção (IllegalArgumentExceptionouTypeCastException).
No construtor da nossa classe, invocamos o construtor da superclasse AbstractDataType fornecendo quatro parâmetros fundamentais:
- O nome do tipo SQL suportado pelo nosso tipo DbUnit (
"json"). - O código do tipo SQL JDBC correspondente (
Types.OTHER). - A classe Java que representa o valor em memória (
String::class.java). - Um booleano indicando se o tipo suporta valores numéricos (
false).
Após sobrescrevermos o método typeCast() e o construtor, o código-fonte da nossa classe JsonDataType em Kotlin 2.4.21 apresenta a seguinte estrutura:
|
|
Suporte a JSONB no PostgreSQL:
O PostgreSQL oferece dois formatos de armazenamento de JSON:json(que preserva o texto exato, incluindo espaços e ordem de chaves) ejsonb(formato binário decomposto, indexável via índices GIN e muito mais performático para buscas). Ambos recebem a mesma representação textual via JDBC ao inserir valores. A classeJsonDataTypeacima funciona com perfeição para ambos os casos.
Agora que criamos o nosso tipo customizado, precisamos descobrir como habilitá-lo no DbUnit.
4. Habilitando Nosso Tipo de Dado Customizado no DbUnit
Para garantir que o DbUnit utilize o nosso tipo de dado customizado, precisamos criar uma fábrica de tipos de dados (data type factory) que estenda a fábrica padrão do PostgreSQL e implementar o método createDataType() da interface IDataTypeFactory.
Podemos implementar esse método seguindo estes passos:
- Se o tipo de dado da coluna processada for
json(oujsonb), retornamos uma nova instância da nossa classeJsonDataType. - Caso contrário, invocamos o método
createDataType()da superclasse (PostgresqlDataTypeFactory) e retornamos o tipo de dado retornado por ela. Isso assegura que o DbUnit continue manipulando todos os tipos padrão do banco de dados (como inteiros, datas e varchars).
Após implementarmos o método createDataType(), o código-fonte da classe CustomPostgreSQLDataTypeFactory em Kotlin 2.4.21 fica assim:
|
|
5. Configurando a Conexão do DbUnit no Spring Boot
Para que o framework spring-test-dbunit utilize a nossa fábrica customizada CustomPostgreSQLDataTypeFactory, precisamos expor os beans de configuração do DbUnit no contexto de teste do Spring Boot.
Configuramos dois beans principais em uma classe de configuração de testes:
- Um bean
DatabaseConfigBeanconfigurado com a nossa fábricaCustomPostgreSQLDataTypeFactory. - Um bean
DatabaseDataSourceConnectionFactoryBeanque associa oDataSourceativo do PostgreSQL (provido pelo Testcontainers) à nossa configuração customizada do DbUnit.
|
|
Também criamos a meta-anotação @DatabaseIntegrationTest, que encapsula a infraestrutura necessária para carregar o contexto da aplicação, a configuração do banco e o listener de execução do DbUnit:
|
|
6. Mapeamento de Domínio com Kotlin e Jackson
No lado da aplicação, criamos nosso modelo de domínio em Kotlin utilizando data classes imutáveis. O Jackson Kotlin Module permite serializar e deserializar o documento JSONB de forma transparente:
|
|
A entidade JPA Product armazena a lista de avaliações mapeada diretamente para a coluna reviews do PostgreSQL via anotação de conversão ou tipo JSON do Hibernate 7 / Spring Boot 4:
|
|
E a interface de repositório do Spring Data JPA:
|
|
7. Escrevendo o Teste de Integração com JUnit 6 e AssertJ
Com a infraestrutura de tipos do DbUnit e o modelo de domínio configurados, escrevemos o teste de integração para verificar se o Spring Data JPA consegue carregar o registro populado pelo DbUnit e se o Jackson deserializa corretamente o documento JSONB:
|
|
Durante a execução deste teste:
- O Testcontainers assegura que o container PostgreSQL esteja em execução.
- O listener do DbUnit lê o dataset
products.xml. - Ao encontrar a coluna
reviews, o DbUnit consultaCustomPostgreSQLDataTypeFactory, que retorna nossoJsonDataType. - O
JsonDataTypevalida que o conteúdo é umaStringe o entrega ao driver JDBC do PostgreSQL como tipo compatível. - O Hibernate/Spring Data lê o registro do PostgreSQL e o Jackson Kotlin Module reconstrói a lista tipada
List<Review>sem necessidade de conversores manuais adicionais.
Resumo e Pontos-Chave
Nesta aula aprendemos como tratar colunas do tipo JSON/JSONB do PostgreSQL em testes de integração:
- Representação Textual no Dataset: Ao definir o valor de uma coluna que armazena um documento JSON em um dataset XML do DbUnit, devemos utilizar a representação textual do JSON, escapando as aspas com
". - Criação de
JsonDataType: EstendemosAbstractDataTypeconfigurando o nomejson(oujsonb), o códigoTypes.OTHERe garantindo no métodotypeCast()que valoresStringsejam aceitos diretamente. - Registro na Fábrica de Tipos: Estendemos
PostgresqlDataTypeFactorye sobrescrevemos o métodocreateDataType(). Se o tipo SQL forjsonoujsonb, retornamos nossa instância deJsonDataType; para os demais tipos, delegamos à superclasse. - Configuração no Spring Boot: Registramos a fábrica customizada via
DatabaseConfigBeane associamos aoDataSourcegerenciado nos testes pelo Testcontainers. - Mapeamento Tipado em Kotlin: Em conjunto com Jackson Kotlin Module e
@JdbcTypeCode(SqlTypes.JSON), o documento JSONB é convertido de/para classes de dados Kotlin imutáveis de forma elegante e segura.