Inicializando Cenários de Dados nos Testes de Integração
Ao concluir esta lição, você será capaz de:
- Saber como inicializar o nosso banco de dados em um estado conhecido antes da execução dos testes de integração (tanto via
@DatabaseSetupdo DbUnit quanto via@Sqlnativo do Spring Test). - Compreender como escrever asserções declarativas e programáticas para os dados encontrados no banco de dados após a execução do método testado.
Esta lição assume que:
1. Especificando a Localização do Dataset Utilizado
Podemos especificar a localização de um dataset do DbUnit (ou script @Sql) seguindo estas duas regras fundamentais de resolução de classpath na JVM:
- Mesmo pacote da classe de teste: Se o arquivo de dataset estiver localizado no mesmo pacote (diretório correspondente em
src/integrationTest/resources) que a classe de teste de integração, podemos especificar sua localização informando apenas o nome do arquivo. - Pacote diferente da classe de teste: Se o dataset não estiver no mesmo pacote da classe de teste de integração, devemos especificar sua localização usando o caminho completo (absoluto no classpath) do arquivo.
Por exemplo, se o nome do nosso arquivo de dataset for data-set.xml (ou data-set.sql), especificamos sua localização seguindo estas regras:
- Se o arquivo estiver no mesmo pacote que nossa classe de teste de integração, usamos a string relativa:
"data-set.xml". - Se o arquivo não estiver no mesmo pacote da classe de teste e pertencer ao pacote base
com.testwithspring.integrationtest, usamos a string com barra inicial:"/com/testwithspring/integrationtest/data-set.xml".
A seguir, veremos como configurar o dataset responsável por inserir os dados de teste no banco de dados.
2. Inserindo Dados de Teste no Nosso Banco de Dados
Quando configuramos o dataset do DbUnit (ou script @Sql) que inicializa nosso banco de dados em um estado conhecido antes de cada teste, devemos escolher uma destas três opções:
Primeiro, se todos os métodos de teste da nossa classe utilizarem exatamente o mesmo conjunto de dados inicial, podemos especificar o dataset anotando a própria classe de teste com @DatabaseSetup (ou @Sql).
Por exemplo, se quisermos usar um dataset que não está no mesmo pacote da nossa classe de teste e reside em com.testwithspring.integrationtest, o código-fonte da nossa classe de teste em Kotlin ficará assim:
|
|
Segundo, se cada método de teste da nossa classe precisar de um cenário de dados diferente, devemos especificar os datasets anotando individualmente os métodos de teste com @DatabaseSetup (ou @Sql).
Por exemplo, se quisermos usar datasets localizados no mesmo pacote da nossa classe de teste, o código-fonte ficará assim:
|
|
Terceiro, se quisermos compor o estado inicial do banco combinando múltiplos datasets modulares (por exemplo, um arquivo para usuários e outro para tarefas ou mensagens), podemos passar múltiplos caminhos na anotação @DatabaseSetup (ou @Sql).
Por exemplo, se todos os métodos da nossa classe de teste usarem a mesma combinação de datasets localizados no mesmo pacote da classe, o código ficará assim:
|
|
Quando utilizamos múltiplos datasets, o DbUnit (assim como o @Sql) processa os arquivos rigorosamente na ordem em que foram declarados. Em outras palavras, nossos datasets serão processados nesta sequência:
messages-1.xmlmessages-2.xml
Nota: Podemos utilizar essa mesma técnica de múltiplos arquivos mesmo quando anotamos métodos de teste individuais com
@DatabaseSetup.
Além disso, se não especificarmos explicitamente a operação (DatabaseOperation) usada para inicializar o banco de dados, o Spring Test DbUnit utiliza seu comportamento padrão (DatabaseOperation.CLEAN_INSERT), que executa as duas operações a seguir antes do teste:
- Remove todas as linhas das tabelas mencionadas nos datasets informados (em ordem reversa para respeitar chaves estrangeiras).
- Insere os dados de teste definidos nos datasets no banco de dados.
Embora o comportamento padrão seja ideal para a imensa maioria dos casos de uso, podemos customizar a DatabaseOperation utilizada especificando o atributo type da anotação @DatabaseSetup.
Por exemplo, se quisermos apenas atualizar registros existentes usando DatabaseOperation.UPDATE, o código-fonte da nossa classe de teste ficará assim:
|
|
Leitura Adicional:
Vamos avançar e descobrir como escrever asserções para os dados encontrados no banco de dados após a execução de operações de escrita.
3. Escrevendo Asserções para os Dados Encontrados no Banco de Dados
Quando queremos verificar declarativamente o estado do banco de dados ao final de um método de teste com DbUnit, anotamos o método de teste com @ExpectedDatabase. Ao usar essa anotação, normalmente especificamos os valores destes dois atributos:
- O atributo
valueespecifica a localização do dataset contendo o estado esperado. - O atributo
assertionModeespecifica o modo de asserção utilizado na comparação.
Vejamos dois exemplos que demonstram como utilizar esses atributos na prática.
Primeiro, especificamos a localização do dataset esperado seguindo exatamente as mesmas regras de caminho relativo ou absoluto descritas no início desta lição.
Por exemplo, se quisermos verificar dois resultados esperados distintos em dois métodos de teste, nossa classe de teste ficará assim:
|
|
Regras de Ouro para Datasets de Asserção:
Ao criar e especificar datasets de verificação (
@ExpectedDatabase), devemos seguir estas três regras:
- Devemos usar datasets pequenos que validem apenas um conceito lógico por teste.
- Devemos colocar os datasets de asserção no mesmo pacote da nossa classe de teste de integração.
- Devemos nomear nossos arquivos de dataset esperado seguindo a convenção:
[nome-do-caso-de-teste]-[resultado-esperado]-expected.xml.
Segundo, nunca devemos usar o modo de asserção padrão (DatabaseAssertionMode.DEFAULT) porque ele tenta verificar o esquema e o conteúdo do banco de dados inteiro (incluindo tabelas de migração como databasechangelog do Liquibase/Flyway e colunas que não interessam ao teste). Se usarmos o modo padrão, nossos datasets ficarão gigantescos, frágeis e ilegíveis. Isso significa que devemos sempre escolher um destes dois modos de asserção:
DatabaseAssertionMode.NON_STRICT: Permite criar datasets enxutos que declaram apenas tabelas e colunas específicas relevantes para o teste. O DbUnit ignora quaisquer tabelas e colunas não especificadas no XML esperado.DatabaseAssertionMode.NON_STRICT_UNORDERED: É uma extensão do modoNON_STRICTque também ignora a ordem física em que as linhas são retornadas pelo banco ao comparar o dataset esperado com o estado real da tabela.
Por exemplo, se quisermos escrever uma asserção que utiliza o DatabaseAssertionMode.NON_STRICT, o código-fonte da nossa classe de teste de integração ficará assim:
|
|
Alternativa Moderna com
JdbcCliente AssertJ:Em projetos Spring Boot 4.1 que optam por usar
@Sqlem vez de XMLs do DbUnit, aplicamos exatamente o mesmo princípio deNON_STRICTconsultando apenas as colunas do conceito lógico testado viaJdbcCliente AssertJ:
1 2 3 4 5val row = jdbcClient.sql("SELECT title, status FROM tasks WHERE id = :id") .param("id", 1L) .query { rs, _ -> rs.getString("title") to rs.getString("status") } .single() assertThat(row).isEqualTo("Write example application" to "CLOSED")
Leitura Adicional:
Vamos resumir o que aprendemos nesta lição.
Resumo
Esta lição nos ensinou quatro pontos essenciais:
- Se o dataset do DbUnit (ou script
@Sql) estiver no mesmo pacote da classe de teste de integração, podemos especificar sua localização informando apenas o nome do arquivo. - Se o dataset não estiver no mesmo pacote da classe de teste de integração, devemos especificar sua localização usando o caminho absoluto completo no classpath.
- Ao inicializar nosso banco de dados em um estado conhecido, na maioria das vezes não precisamos sobrescrever a
DatabaseOperationpadrão (CLEAN_INSERT). - Ao escrever asserções para os dados encontrados no banco de dados com
@ExpectedDatabase, nunca devemos usar o modo de asserção padrão — devemos sempre optar porDatabaseAssertionMode.NON_STRICTouNON_STRICT_UNORDERED.