Introdução à Preparação de Datasets de Banco de Dados
Ao concluirmos esta aula, nós:
- Saberemos o que é o DbUnit e como ele se compara com a abordagem nativa moderna (
@Sqle AssertJ / JdbcClient) do Spring Boot 4. - Saberemos como obter as dependências necessárias no Maven e no Gradle 9.
- Compreenderemos o esquema relacional do banco de dados da nossa aplicação de exemplo (Task Tracker).
- Saberemos como criar datasets que inicializam o banco de dados em um estado conhecido antes da execução de cada teste e verificam os dados persistidos após o teste.
O Que É o DbUnit (e a Evolução para @Sql + Testcontainers)?
Quando escrevemos testes de integração que interagem com um banco de dados relacional (como PostgreSQL), enfrentamos dois desafios essenciais:
- Inicializar o banco de dados em um estado conhecido antes que o método de teste seja executado.
- Escrever asserções sobre os dados encontrados no banco de dados após o método de teste ter terminado sua execução (quando testamos operações de escrita como
INSERT,UPDATEouDELETE).
No ecossistema Java/Spring, existem duas abordagens consagradas para resolver esses desafios:
- Abordagem Clássica com DbUnit (
org.dbunit:dbunit+spring-test-dbunit): Uma extensão do JUnit que lê datasets em formato Flat XML, limpa as tabelas em ordem reversa, insere os registros antes do teste (@DatabaseSetup) e compara o estado final das tabelas com um XML esperado (@ExpectedDatabase). - Abordagem Nativa Moderna do Spring Boot 4 (
@Sql+JdbcClient/ AssertJ): Executa scripts SQL determinísticos antes do teste via@Sqle verifica o estado final consultando o banco ou os repositórios com asserções fluentes do AssertJ.
Neste tópico, estudaremos em detalhes tanto o funcionamento dos datasets declarativos do DbUnit (incluindo a ordem de limpeza, column sensing e replacement datasets) quanto sua equivalência direta em SQL nativo (@Sql), pois os princípios arquiteturais de organização de dados de teste aplicam-se igualmente a ambos!
Obtendo as Dependências Necessárias
Para utilizarmos o DbUnit em conjunto com o suporte a banco de dados do Spring Test, declaramos a dependência no nosso script de build.
Se estivermos usando o Maven (pom.xml):
|
|
Se estivermos usando o Gradle 9 (Kotlin DSL — build.gradle.kts):
|
|
Vamos conhecer agora o formato de datasets do DbUnit e o esquema de banco de dados da nossa aplicação!
Introdução aos Datasets Flat XML do DbUnit
Utilizamos o formato Flat XML do DbUnit porque ele é muito mais conciso e fácil de ler do que o formato XML verboso original. As regras para criar um dataset Flat XML são:
- O arquivo XML deve ter um elemento raiz chamado
<dataset>. - O elemento raiz pode conter múltiplos elementos filhos.
- Cada elemento filho corresponde a uma única linha (registro) de uma tabela do banco de dados. O nome da tag XML especifica o nome da tabela (por exemplo,
<tasks ... />). - Os atributos de cada elemento descrevem os valores das colunas daquela linha (por exemplo,
id="1" title="foo"). - Se quisermos especificar que uma tabela do banco de dados deve estar vazia (zero linhas), adicionamos um elemento vazio sem atributos (por exemplo,
<tags/>).
Introdução ao Banco de Dados da Nossa Aplicação de Exemplo
O diagrama a seguir descreve a estrutura relacional do banco de dados da nossa aplicação de gerenciamento de tarefas (Task Tracker):
Como podemos observar, o nosso banco de dados possui quatro tabelas:
Primeiro, a tabela persons armazena os dados das pessoas que utilizam o nosso gerenciador de tarefas. Ela possui as seguintes colunas:
id: Identificador único da pessoa (chave primária).email_address: Endereço de e-mail da pessoa.name: Nome completo da pessoa.password: Senha (hash) da pessoa.user_role: Papel de autorização da pessoa (ex:ROLE_USER,ROLE_ADMIN).
Segundo, a tabela tasks armazena as informações das nossas tarefas. Ela possui as seguintes colunas:
id: Identificador único de cada tarefa (chave primária).assignee_id: Identifica a pessoa responsável pela tarefa (persons.id, opcional).closer_id: Identifica a pessoa que encerrou a tarefa (persons.id, opcional).creator_id: Identifica a pessoa que criou a tarefa (persons.id, obrigatório).description: Descrição detalhada da tarefa.title: Título da tarefa.status: Status atual da tarefa (OPEN,IN_PROGRESS,CLOSED).resolution: Resolução da tarefa quando encerrada (DONE,DUPLICATE,WONT_DO, opcional).
Terceiro, a tabela tags armazena as etiquetas que podem ser associadas às tarefas:
id: Identificador único da tag (chave primária).name: Nome da tag.
Quarto, a tabela tasks_tags é uma tabela de junção (join table N:N) que vincula tarefas a tags:
task_id: Identifica a tarefa (tasks.id).tag_id: Identifica a tag (tags.id).
Vamos descobrir agora como criar datasets que inserem informações nesse banco de dados!
Inserindo Dados no Nosso Banco de Dados
Vamos analisar três exemplos práticos que demonstram como o DbUnit (e scripts SQL equivalentes) limpa e popula as tabelas do banco de dados.
Exemplo 1: Uma Tarefa Aberta que Não Possui Tags
Se quisermos garantir que o nosso banco de dados contenha exatamente uma tarefa aberta (OPEN) e nenhuma tag, usamos o seguinte dataset:
|
|
Quando utilizamos este dataset antes de um método de teste, o DbUnit executa duas operações:
Primeiro, ele exclui (DELETE) todas as linhas encontradas nas tabelas mencionadas no dataset, percorrendo-as em ordem reversa. Ou seja, o DbUnit limpa as tabelas exatamente nesta ordem: tasks_tags, tags e por último tasks (evitando violações de chave estrangeira!).
Segundo, ele insere uma linha na tabela tasks. A instrução INSERT executada no banco de dados é equivalente a:
|
|
Observe que todas as colunas omitidas no elemento XML <tasks> (como assignee_id, closer_id e resolution) receberão o valor NULL no banco de dados.
Exemplo 2: Uma Tarefa Aberta que Possui Uma Tag
Se quisermos garantir que o nosso banco de dados possua uma tarefa aberta associada a uma tag, utilizamos o seguinte dataset:
|
|
Quando utilizamos este dataset, o DbUnit realiza as seguintes operações:
Primeiro, o DbUnit limpa as tabelas em ordem reversa: tasks_tags, tags e tasks.
Segundo, o DbUnit insere os registros exatamente na ordem em que foram declarados no arquivo XML, executando os seguintes comandos INSERT:
|
|
Portanto, para evitarmos erros de violação de integridade referencial (foreign key constraint violation), devemos sempre declarar as linhas das tabelas pai (persons, tasks, tags) antes das tabelas filhas (tasks_tags)!
Exemplo 3: Duas Tarefas com Atributos Diferentes (Column Sensing)
Suponha que queiramos inserir duas tarefas no banco de dados, sendo que apenas a segunda tarefa foi atribuída a um responsável (assignee_id="1"):
|
|
Como os dois elementos <tasks> possuem conjuntos diferentes de atributos XML (o primeiro não tem assignee_id, enquanto o segundo tem), o que é inserido no banco de dados depende do recurso chamado Column Sensing do DbUnit:
Primeiro, se não habilitarmos o column sensing, o DbUnit olha apenas para os atributos do primeiro elemento <tasks> do arquivo para decidir quais colunas farão parte do INSERT! Isso significa que o atributo assignee_id="1" da segunda tarefa seria silenciosamente ignorado:
|
|
Segundo, se habilitarmos o column sensing (o que faremos quando configurarmos nossa integração com o Spring Test!), o DbUnit lê todo o arquivo XML antes de montar os comandos INSERT. Com o column sensing ativo, ele executa os INSERTs corretos:
|
|
Escrevendo Asserções para os Dados Encontrados no Banco de Dados
Vamos analisar dois exemplos que demonstram como o DbUnit verifica o estado do banco de dados após a execução de um teste.
Exemplo 1: Verificando Uma Tarefa Sem Tags
Se quisermos verificar que o nosso banco de dados contém uma tarefa aberta com título "foo", descrição "bar" e nenhuma tag, podemos usar o seguinte dataset esperado:
|
|
Ao comparar este dataset com o estado real do banco de dados, o DbUnit garante que:
- A tabela
tasks_tagspossui zero linhas. - A tabela
tagspossui zero linhas. - A tabela
taskspossui uma linha contendo os valores esperados paraid,creator_id,description,titleestatus. Importante: se uma coluna da tabela não for especificada no XML esperado, o DbUnit simplesmente ignora aquela coluna durante a comparação! Ou seja, o XML acima não verifica seassignee_id,closer_idouresolutionsãoNULL!
E se quisermos verificar explicitamente que o valor de uma coluna no banco de dados é NULL? Para isso, utilizamos um Replacement Data Set (Dataset de Substituição), definindo um marcador como [null] (ex: assignee_id="[null]"), que veremos em detalhes nas próximas aulas!
Exemplo 2: Verificando Duas Tarefas
Se quisermos verificar que o banco de dados possui duas tarefas, usamos:
|
|
Note que, ao escrevermos asserções com datasets esperados do DbUnit, todas as linhas de uma mesma tabela precisam declarar os mesmos atributos (ou usar [null] nas linhas onde a coluna for nula).
Resumo
Esta aula nos ensinou cinco conceitos essenciais:
- O DbUnit é uma biblioteca que auxilia na escrita de testes de integração automatizados que manipulam bancos de dados relacionais (em conjunto ou como alternativa ao
@Sqldo Spring Test). - Ao inserir dados no banco, o DbUnit limpa as tabelas presentes no dataset em ordem reversa para respeitar as chaves estrangeiras.
- Ao inserir dados no banco, o DbUnit executa as instruções
INSERTna ordem exata em que as linhas aparecem no arquivo. - Ao escrever asserções sobre o banco de dados, o DbUnit ignora as colunas que foram omitidas no dataset esperado.
- Para que múltiplas linhas da mesma tabela possam especificar valores nulos e não-nulos de forma consistente, devemos habilitar o column sensing e utilizar replacement datasets (
[null]).