Testando Colunas TEXT[] (Arrays PostgreSQL) em Testes
Ao finalizarmos esta aula, seremos capazes de:
- Escrever um tipo de dado customizado (custom DbUnit data type) que ofereça suporte nativo a arrays de texto (
text[]) do PostgreSQL. - Compreender e configurar os mecanismos necessários para garantir que o DbUnit utilize nossa fábrica e nosso tipo de dados customizado durante a carga de data sets.
Esta aula assume que:
Obtenha o código-fonte no GitHub
O Desafio: Arrays Nativos no PostgreSQL (text[]) e o DbUnit
O PostgreSQL possui suporte de primeira classe a tipos de array multidimensionais, como text[], varchar[], integer[] e bigint[]. No desenvolvimento de aplicações modernas com Spring Boot 4.1.2, Kotlin 2.4.21 e Hibernate 7 ou jOOQ, o uso de arrays nativos é comum para armazenar listas simples (como tags de produtos, permissões ou categorias) sem a necessidade de criar tabelas associativas adicionais.
No entanto, ao escrever testes de integração com o DbUnit para verificar repositórios que persistem ou consultam essas tabelas, nos deparamos com uma limitação: a implementação padrão PostgresqlDataTypeFactory fornecida pelo DbUnit não sabe como converter uma string declarada em um arquivo XML ou JSON de data set em um array JDBC compatível (java.sql.Array ou Array<String>).
Se tentarmos carregar um valor comum em uma coluna tags do tipo text[], o driver JDBC do PostgreSQL lançará uma exceção semelhante a:
|
|
Para contornar essa situação e manter nossos data sets limpos e declarativos, ensinaremos o DbUnit a converter automaticamente uma lista delimitada por vírgulas em um array nativo de strings antes de enviar a instrução ao PostgreSQL.
Criando Nosso Conjunto de Dados do DbUnit
Precisamos escrever testes de integração para um repositório que lê e manipula informações da tabela products. Quando inicializamos nosso banco de dados em um estado conhecido antes da execução de cada teste, devemos definir o valor da coluna tags, que armazena um array de texto (text[]).
Ao configurar o valor dessa coluna em nosso data set do DbUnit, a escolha mais intuitiva, legível e sucinta é utilizar uma sequência de valores separados por vírgula (comma-separated value) especificando as tags de cada produto:
|
|
A seguir, aprenderemos como criar um tipo de dado customizado no DbUnit que reconheça essa formatação e faça o casting adequado.
Escrevendo Nosso Tipo de Dados Customizado do DbUnit
Quando queremos criar um tipo de dado customizado no DbUnit, devemos criar uma nova classe estendendo AbstractDataType e sobrescrever o método typeCast() da classe DataType.
Podemos implementar esse método seguindo estes dois passos:
- Se o valor fornecido como parâmetro do método for um objeto
String, transformamos esse valor em um array de strings (Array<String>/String[]) e retornamos o array criado. - Se o valor fornecido como parâmetro do método não for um objeto
String, lançamos uma nova exceção informando que o tipo recebido não é suportado.
Observe também o construtor da nossa classe: invocamos o construtor da superclasse AbstractDataType passando o nome do tipo SQL, o código numérico JDBC (Types.ARRAY), a classe Java correspondente (String::class.java ou Array<String>::class.java) e a flag booleana false (indicando que não se trata de um número).
Após implementarmos a classe StringArrayDataType em Kotlin, seu código-fonte fica assim:
|
|
Como o Driver JDBC do PostgreSQL processa esse retorno: O driver JDBC oficial do PostgreSQL (
org.postgresql.Driver) aceita diretamente instâncias deString[]/Array<String>no métodoPreparedStatement.setObject(index, value). Ao converter a string do XML em umtoTypedArray(), o DbUnit delega o array nativo de strings para o driver, que mapeia perfeitamente a coleção para o tipotext[]do PostgreSQL.
Com o tipo de dado criado, precisamos agora garantir que o DbUnit o utilize ao processar as colunas de tabela do tipo array.
Habilitando Nosso Tipo de Dados Customizado no DbUnit
Para garantir que o DbUnit utilize nosso tipo de dados customizado, precisamos criar uma nova classe de fábrica de tipos de dados (data type factory) estendendo a fábrica padrão do PostgreSQL (PostgresqlDataTypeFactory) e sobrescrever o método createDataType() da interface IDataTypeFactory.
Podemos implementar o método createDataType() seguindo estes três passos:
- Se o tipo JDBC da coluna processada for um array (
sqlType == Types.ARRAY), verificamos se o nome do tipo SQL coincide com o identificador interno do PostgreSQL para arrays de texto (representado pela constante literal"_text"). Caso afirmativo, retornamos uma nova instância deStringArrayDataType. - Se o tipo JDBC da coluna for um array, mas com um tipo não suportado por nossa fábrica, lançamos uma exceção
IllegalArgumentExceptioninformando o nome do tipo rejeitado. - Se a coluna não for do tipo array, invocamos o método
createDataType()da superclasse (PostgresqlDataTypeFactory) e retornamos o tipo de dado padrão resolvido por ela. Isso garante que todos os demais tipos primitivos e estruturas padrão do PostgreSQL continuem funcionando sem nenhuma regressão.
Após implementarmos o método createDataType(), o código-fonte da nossa classe de fábrica customizada fica da seguinte forma:
|
|
Por que o nome do tipo é
_text? No catálogo interno do PostgreSQL (pg_type), os tipos de array nativos recebem como convenção de nomenclatura o prefixo sublinhado seguido do tipo de elemento base (por exemplo,_textparatext[],_int4parainteger[]e_varcharparavarchar[]). Quando o driver JDBC inspeciona os metadados do banco (ResultSetMetaData.getColumnTypeName), ele retorna o identificador interno_text.
A seguir, veremos como registrar essa fábrica no contexto de testes do Spring Boot 4.1.2 e utilizá-la em um teste de integração completo.
Configuração do DbUnit no Spring Boot 4.1.2 e Testcontainers
Para que os testes de integração carreguem nossa fábrica customizada, definimos uma classe de configuração do Spring com anotação @Configuration (ou @TestConfiguration), instanciando os beans de configuração do DbUnit:
|
|
Anotação Customizada de Teste de Integração
Combinamos a configuração de banco de dados, o ouvinte de execução do DbUnit (DbUnitTestExecutionListener) e o perfil de integração em uma meta-anotação reutilizável:
|
|
Exemplo Prático: Repositório e Teste de Integração com JUnit Jupiter 6.0.3
Vejamos como o modelo de entidade e o teste de repositório se comportam na prática.
Entidade Product
No Spring Data / Hibernate 7 com PostgreSQL, podemos mapear arrays utilizando anotações nativas ou conversores de tipo:
|
|
Repositório Spring Data JPA
|
|
Teste de Integração de Repositório
Agora, escrevemos o teste de integração utilizando JUnit Jupiter 6.0.3, AssertJ 3.27.7 e o conjunto de dados XML que criamos:
|
|
Quando o método shouldLoadProductWithTagsFromDataSet() é executado:
- O Spring TestContext inicializa o contêiner PostgreSQL via Testcontainers.
- O
DbUnitTestExecutionListenerintercepta a anotação@DatabaseSetup("/datasets/products.xml"). - O DbUnit utiliza o bean
dbUnitDatabaseConfig, que referencia nossaCustomPostgreSQLDataTypeFactory. - Ao encontrar a coluna
tagscom metadado SQL_text, a fábrica instanciaStringArrayDataType. - O método
typeCast("example,product")processa a string e devolvearrayOf("example", "product"). - O driver JDBC do PostgreSQL recebe o array e popula a coluna
text[]com perfeição. - O teste executa
productRepository.findById(1L)e valida os valores com asserções limpas e expressivas.
Resumo
Nesta aula, aprendemos:
- Ao definir o valor de uma coluna de banco de dados que contém um array de texto (
text[]), a maneira mais concisa e legível é utilizar valores separados por vírgula em nosso data set XML do DbUnit. - Para habilitar o DbUnit a entender essa notação, criamos a classe
StringArrayDataType, herdando deAbstractDataTypee implementando o métodotypeCast(), transformando aStringde entrada em um array tipado (Array<String>). - Para instruir o DbUnit a utilizar nosso tipo customizado, criamos a fábrica
CustomPostgreSQLDataTypeFactory, herdando dePostgresqlDataTypeFactorye sobrescrevendocreateDataType(). - Se o tipo JDBC for
Types.ARRAYe o nome SQL for_text, retornamos uma nova instância deStringArrayDataType. Para tipos de array não suportados, lançamos uma exceção; para todos os demais tipos de coluna, delegamos a chamada para o método da superclasse. - Configuramos a fábrica customizada em nosso contexto de teste do Spring Boot 4.1.2 por meio dos beans
DatabaseConfigBeaneDatabaseDataSourceConnectionFactoryBean.