Boas Práticas: Asserções para o Estado do Banco
Ao concluirmos esta aula, nós:
- Compreenderemos por que não devemos escrever nossas asserções de banco de dados usando um único dataset gigante por caso de teste.
- Seremos capazes de identificar os três requisitos de uma boa verificação de estado no banco de dados.
- Saberemos como dividir nossas verificações de banco de dados (seja com
@ExpectedDatabasedo DbUnit ou com queries AssertJ) para que cada teste valide apenas um conceito lógico de negócio.
O Erro Mais Comum ao Verificar Dados no Banco
Um padrão muito comum em testes de integração que salvam ou atualizam registros no banco de dados é tentar verificar todo o estado do banco de dados de uma só vez em um único método de teste.
Por exemplo, se quisermos verificar que uma nova tarefa aberta foi salva no banco de dados, muitos desenvolvedores criam um dataset esperado (@ExpectedDatabase) parecido com este:
|
|
Nota sobre
[null]: O dataset acima é um replacement dataset (dataset de substituição). A string[null]é substituída pelo valornullquando o DbUnit compara o conteúdo real do banco de dados com os valores especificados no XML.
À primeira vista, esse dataset pode não parecer tão ruim porque possui poucas linhas. Contudo, à medida que a aplicação cresce, essa abordagem sofre de dois problemas graves:
- Ele contém dados irrelevantes: Por que um teste que verifica a criação de uma tarefa está validando todas as colunas da tabela
persons? Asserções desnecessárias tornam o teste mais lento, mais frágil a mudanças em outras tabelas e, acima de tudo, poluem a leitura do teste. - Ele esconde a essência das regras de negócio do caso de teste: Você já reparou que testes que usam essa abordagem costumam ter nomes genéricos como
shouldSaveTaskToDatabase()? Como o dataset tenta verificar uma dezena de regras de negócio simultaneamente, torna-se impossível dar um nome específico ao método de teste ou saber rapidamente qual regra de negócio quebrou quando o teste falha!
É evidente que, ao escrevermos asserções sobre os dados encontrados no banco de dados, não devemos verificar cegamente todas as colunas de todas as tabelas em um único teste. Vamos descobrir como escrever asserções melhores!
Escrevendo Asserções de Banco de Dados Limpas e Focadas
Uma boa verificação de estado de banco de dados (seja via dataset esperado do DbUnit ou via asserção AssertJ no repositório/JdbcClient) cumpre três requisitos:
- Ela nos ajuda a identificar imediatamente as tabelas relevantes: Uma tabela do banco de dados só é relevante para o nosso caso de teste se o seu conteúdo puder ser modificado pelo código sob teste ou se ela for explicitamente mencionada nos requisitos de negócio da funcionalidade testada.
- Ela testa apenas um único conceito lógico: Um dataset esperado é, em essência, uma lista de asserções declaradas em formato XML (ou SQL/Kotlin). Se concordamos que um método de teste unitário deve focar em um único conceito lógico, o mesmo princípio vale para os nossos testes de integração de banco de dados!
- Ela facilita o diagnóstico imediato quando o teste falha: Um bom caso de teste é escrito para seres humanos lerem. Quando o teste falha, o nome do teste e o dataset enxuto devem apontar exatamente qual invariante de domínio foi violada.
Isso nos leva à pergunta prática:
Como refatoramos o nosso dataset original para cumprir esses três requisitos?
Vamos transformar o nosso dataset original passo a passo! Como lembramos, o dataset original tentava verificar tudo ao mesmo tempo:
|
|
Passo 1: Removendo Tabelas Irrelevantes
Como a funcionalidade de “criar uma nova tarefa” não faz nenhuma alteração na tabela persons, não precisamos verificar seu conteúdo. Após removermos a tabela persons, restam apenas as tabelas do agregado de tarefas:
|
|
Passo 2: Dividindo por Conceito Lógico de Negócio
Agora dividimos essa verificação em cinco conceitos lógicos claros e independentes:
1. Verificar que a tarefa criada não possui tags (create-task-should-not-create-tags.xml):
|
|
2. Verificar que o criador da tarefa salva está correto (create-task-should-set-creator.xml):
|
|
3. Verificar que a tarefa salva possui o título e a descrição corretos (create-task-should-save-title-and-description.xml):
|
|
4. Verificar que a tarefa foi salva com status aberta (OPEN) e não encerrada (create-task-should-save-open-task.xml):
|
|
5. Verificar que a tarefa recém-criada não está atribuída a ninguém (create-task-should-not-assign-task.xml):
|
|
Comparando com Asserções Modernas em Kotlin (AssertJ + assertSoftly)
Quando utilizamos JdbcClient ou repositórios Spring Data JPA com AssertJ no Spring Boot 4 e Kotlin, podemos expressar exatamente esses mesmos conceitos lógicos de forma ainda mais concisa e sem múltiplos arquivos XML:
- Ou escrevendo métodos
@Testseparados para cada conceito lógico (shouldSaveTaskWithCorrectTitleAndDescription(),shouldSaveOpenTask(),shouldNotAssignTaskToAnyone()). - Ou agrupando cada conceito lógico com descrições claras dentro de
assertSoftlyquando quisermos executar a transação de integração uma única vez!
Ao compararmos o dataset monolítico antigo com a abordagem dividida por conceito lógico, observamos três pontos importantes:
- Nossos datasets de verificação ficam muito menores, sem ruído visual e fáceis de ler.
- Cada verificação destaca uma regra de negócio específica da funcionalidade de criação de tarefas.
- Há um trade-off consciente entre ter métodos de teste granulares (que executam a operação de banco 5 vezes) versus agrupar verificações correlacionadas; em suítes com Testcontainers (onde o container PostgreSQL é compartilhado em memória tmpfs), 5 queries levam apenas alguns milissegundos!
Resumo
Esta aula nos ensinou três lições essenciais:
- Se verificarmos todas as tabelas e colunas em um único dataset monolítico, nossos testes deixarão de comunicar as regras de negócio da aplicação.
- Um bom dataset de verificação não contém tabelas irrelevantes e foca em testar apenas um conceito lógico por vez.
- Devemos sempre equilibrar a granularidade e clareza das nossas asserções de banco de dados com o tempo de execução da suíte de testes de integração.