Inicializando o Banco de Dados com Liquibase
Depois de ler esta aula, você entenderá:
- O que é o Liquibase.
- Por que devemos utilizar uma ferramenta de migração de banco de dados quando inicializamos o banco para nossos testes End-to-End.
- Como podemos popular nosso banco de dados com dados de teste utilizando o Liquibase.
Vamos começar fazendo uma rápida revisão sobre o que é o Liquibase.
1. O Que é o Liquibase?
O Liquibase é uma biblioteca open source para rastrear, gerenciar e aplicar mudanças de schema de banco de dados. Ele nos ajuda a construir um histórico de todas as alterações feitas no nosso banco de dados. E o melhor de tudo: como essas alterações são descritas de forma declarativa em arquivos de texto (XML, YAML ou SQL), podemos — e devemos — colocá-las no controle de versão (Git) junto com o código da aplicação.
No Liquibase, as alterações são gerenciadas utilizando um arquivo de changelog (db.changelog.xml) que contém uma lista ordenada de changesets. Cada <changeSet> descreve um conjunto atômico de alterações que será aplicado ao banco de dados alvo.
Para entender como isso funciona na prática, vamos analisar um exemplo simples de arquivo de changelog:
|
|
Quando nossa aplicação é iniciada (ou quando executamos o Liquibase manualmente):
- O Liquibase lê os changesets do arquivo de changelog na mesma ordem em que aparecem no arquivo.
- Para cada changeset, ele consulta a tabela de controle
DATABASECHANGELOGno banco de dados para verificar se aquele changeset (identificado pela combinação deid,authore caminho do arquivo) já foi executado anteriormente. - Se o changeset ainda não tiver sido executado, o Liquibase aplica as alterações descritas nele ao banco de dados e grava um novo registro na tabela
DATABASECHANGELOG.
Mas por que devemos utilizar uma ferramenta de migração como o Liquibase para inicializar nosso banco nos testes End-to-End, em vez de ferramentas de teste como o DbUnit?
2. Por Que Usar uma Ferramenta de Migração de Banco em Testes E2E?
Se estivermos escrevendo testes End-to-End para uma aplicação Spring tradicional (onde o servidor da aplicação é iniciado em um processo separado pelo build antes da execução dos testes) ou quando queremos inicializar um volume base de dados no startup da aplicação, muitas vezes não podemos inicializar nosso banco antes de cada método de teste usando o DbUnit pelos seguintes motivos:
- Não estamos usando o
SpringJUnit4ClassRunner/SpringExtensionacoplado ao contexto interno da aplicação quando a aplicação roda em um container ou processo externo, o que significa que não podemos usar@DatabaseSetupdiretamente no processo do teste sem configurar uma segunda conexão de banco paralela. - Testes End-to-End são lentos por natureza. Se inicializarmos o banco de dados do zero via rede antes de cada método de teste em uma suíte grande, nossos testes ficarão ainda mais lentos.
- Nossa aplicação de exemplo utiliza o Liquibase para criar o schema do banco de dados quando é iniciada. Portanto, faz total sentido aproveitar o próprio Liquibase para popular os dados iniciais de teste quando a aplicação sobe no perfil de testes End-to-End!
Entretanto, como essa abordagem inicializa o banco de dados apenas uma vez (no momento em que a aplicação é iniciada), precisamos tomar quatro cuidados fundamentais ao escrever nossos testes End-to-End:
- Testes que criam novas entidades no banco de dados não devem quebrar testes que buscam dados do banco. Por exemplo, se o teste A cria uma nova tarefa e o teste B verifica a lista de tarefas existentes, devemos garantir que o teste A possa ser executado tanto antes quanto depois do teste B sem quebrar a suíte (ou projetar os filtros/buscas de modo determinístico).
- Dois testes diferentes não devem modificar as informações da mesma entidade. Se ambos alterarem o mesmo registro, a ordem de execução dos métodos de teste determinará qual teste passa e qual falha.
- Se um teste modificar os dados de uma entidade, outro teste não deve depender do estado original dessa mesma entidade. Novamente, isso criaria acoplamento temporal entre os testes.
- Se um teste deletar uma entidade do banco de dados, nenhum outro teste deve utilizar essa entidade. Se utilizarem, o resultado dependerá da ordem de execução dos testes.
Dica Prática: Na próxima aula (Aula 05), veremos também como combinar o Liquibase (para criar o schema) com a anotação
@Sqldo Spring Boot Test (para popular e limpar as tabelas a cada teste quando usamos@SpringBootTest), dando a você o melhor dos dois mundos!
Vamos ver agora como configurar o Liquibase para popular nosso banco nos testes End-to-End.
3. Populando Nosso Banco de Dados com Liquibase
Nossa aplicação de exemplo utiliza o Liquibase para criar as tabelas do banco de dados quando a aplicação é iniciada. Além disso, queremos utilizar o Liquibase para popular nosso banco com dados de teste apenas quando a aplicação for iniciada para o nosso build de testes End-to-End.
Podemos configurar o Liquibase seguindo estes passos:
Passo 1: Adicionar a Dependência do Liquibase
Adicionamos a dependência org.liquibase:liquibase-core ao nosso build.gradle.kts:
|
|
Passo 2: Criar os Scripts SQL com os Dados de Teste
Criamos os scripts SQL que inserem os dados de teste nas tabelas user_accounts e tasks dentro do diretório src/main/resources/db/changelog/test-data/:
Arquivo users.sql (src/main/resources/db/changelog/test-data/users.sql):
|
|
(Nota: A senha criptografada com BCrypt do usuário john.doe@gmail.com corresponde à senha em texto plano "user").
Arquivo tasks.sql (src/main/resources/db/changelog/test-data/tasks.sql):
|
|
Por que executamos
ALTER SEQUENCE tasks_id_seq RESTART WITH 4;? Porque inserimos manualmente 3 tarefas com os IDs explícitos1,2e3. Se não avançarmos a sequence do PostgreSQL/H2 para4, na primeira vez que um teste End-to-End submeter o formulário de criação de nova tarefa, o banco tentará gerar o ID1e falhará com violação de chave primária!
Passo 3: Adicionar um <changeSet> com context="endToEndTest" no Changelog
No arquivo de changelog principal do Liquibase (src/main/resources/db/changelog/db.changelog.xml), adicionamos um novo <changeSet> ao final do arquivo que executa os scripts users.sql e tasks.sql.
Para garantir que esses dados de teste jamais sejam inseridos em ambientes de produção ou desenvolvimento normal, configuramos o atributo context desse changeset como "endToEndTest", enquanto os changesets que criam as tabelas possuem context="application":
|
|
Passo 4: Configurar os Contextos do Liquibase por Perfil
Agora só precisamos dizer ao Liquibase quais contexts ele deve executar em cada ambiente:
- No arquivo
application.propertiespadrão da aplicação (src/main/resources/application.properties), configuramos o Liquibase para executar apenas os changesets pertencentes ao contextoapplication:
|
|
- No perfil ou arquivo de propriedades utilizado pelos testes End-to-End (
src/endToEndTest/resources/application.properties), configuramos o Liquibase para executar ambos os contextosapplicationeendToEndTest:
|
|
Resumo
Nesta aula, aprendemos que:
- O Liquibase nos permite rastrear e aplicar alterações de schema e carga de dados por meio de
<changeSet>s declarativos. - Ao utilizar o atributo
context="endToEndTest"em um<changeSet>dedicado, podemos separar com segurança os scripts SQL de dados de teste das migrações reais de produção. - Sempre que inserirmos registros com IDs fixos em scripts SQL, devemos reiniciar a sequence correspondente (
ALTER SEQUENCE tasks_id_seq RESTART WITH 4;) para que novos inserts feitos pela UI durante os testes End-to-End não colidam com chaves primárias existentes. - Na próxima aula, veremos como podemos inicializar e limpar o banco de dados antes e depois de cada método de teste End-to-End utilizando a anotação
@Sqldo Spring Framework!