Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save othmane-kinane-nw/cc4f8a325343243e1f0cdbbd6018f85c to your computer and use it in GitHub Desktop.
Save othmane-kinane-nw/cc4f8a325343243e1f0cdbbd6018f85c to your computer and use it in GitHub Desktop.
JUnit tests to ensure there's no missing Liquibase migration and that hibernate successfully valide the final schema
import liquibase.CatalogAndSchema;
import liquibase.Contexts;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.diff.output.DiffOutputControl;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.integration.commandline.CommandLineUtils;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import javax.sql.DataSource;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
// Tested with these dependencies:
// - Spring Boot: 3.4.4
// - Spring Framework: 6.2.5
// - Spring Data JPA: 3.4.4
// - Liquibase Core & Hibernate6 Extension: 4.31.1
// - Hibernate Core & Envers: 6.6.11.Final
// - PostgreSQL JDBC Driver: 42.7.5
// - Testcontainers (JUnit Jupiter, PostgreSQL, JDBC): 1.20.6
@Testcontainers
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@DataJpaTest
class LiquibaseMigrationIntegrationTests {
@Container
private static final JdbcDatabaseContainer<?> testDatabase =
new PostgreSQLContainer<>("postgres:17.4");
private static final ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor();
private static final String changeLogFile = "db/changelog-master.yaml";
private static final String hibernateReferenceUrl =
"hibernate:spring:" + BASE_PACKAGE_NAME + "?dialect=org.hibernate.dialect.PostgreSQLDialect";
@Autowired
private DataSource dataSource;
@TempDir
private Path tempDir;
@Autowired
private SessionFactory sessionFactory;
@Test
void liquibase_generate_non_empty_diff_against_fresh_database() throws Exception {
Optional<String> diffAsYamlOptional;
try (Connection connection = dataSource.getConnection()) {
Database targetDatabase = getDatabase(connection);
Database referenceDatabase = getHibernateReferenceDatabase();
diffAsYamlOptional = getDiffAsYaml(referenceDatabase, targetDatabase);
}
// Be aware that on older versions of liquibase generate a diff file containing
// an empty array even if no diff were detected. Please adjust if it's your case.
assertTrue(diffAsYamlOptional.isPresent(), "diff on fresh database is expected to have content, but it was empty.");
}
@Test
void liquibase_generate_empty_diff_after_applying_all_migrations() throws Exception {
Optional<String> diffAsYamlOptional;
try (Connection connection = dataSource.getConnection()) {
Database targetDatabase = getDatabase(connection);
applyMigrations(targetDatabase);
Database referenceDatabase = getHibernateReferenceDatabase();
diffAsYamlOptional = getDiffAsYaml(referenceDatabase, targetDatabase);
}
// Be aware that older versions of liquibase generate a diff file containing
// an empty array even if no diff were detected. Please adjust if it's your case.
assertTrue(diffAsYamlOptional.isEmpty(),
() -> "diff after applying all migrations is expected to be empty, but found this instead:\n\n" + diffAsYamlOptional.orElseThrow());
}
@Test
void hibernate_successfully_validate_schema_created_by_liquibase() throws Exception {
try (Connection connection = dataSource.getConnection()) {
Database targetDatabase = getDatabase(connection);
applyMigrations(targetDatabase);
}
assertDoesNotThrow(() -> sessionFactory.getSchemaManager().validateMappedObjects(),
"hibernate schema validation failed after applying all migrations");
}
@DynamicPropertySource
private static void registerDatasourceProperties(DynamicPropertyRegistry registry) {
if (testDatabase.isCreated()) {
testDatabase.close();
}
testDatabase.start();
registry.add("spring.datasource.url", testDatabase::getJdbcUrl);
registry.add("spring.datasource.username", testDatabase::getUsername);
registry.add("spring.datasource.password", testDatabase::getPassword);
registry.add("spring.liquibase.enabled", () -> "false");
registry.add("spring.jpa.hibernate.ddl-auto", () -> "none");
}
private static Database getHibernateReferenceDatabase() throws DatabaseException {
return DatabaseFactory.getInstance().openDatabase(
hibernateReferenceUrl,
null, null, null, resourceAccessor);
}
private static Database getDatabase(Connection connection) throws DatabaseException {
return DatabaseFactory.getInstance()
.findCorrectDatabaseImplementation(new JdbcConnection(connection));
}
private static void applyMigrations(Database targetDatabase) throws LiquibaseException {
Liquibase liquibase = new Liquibase(changeLogFile, resourceAccessor, targetDatabase);
liquibase.update(new Contexts());
}
private Optional<String> getDiffAsYaml(Database referenceDatabase, Database targetDatabase) throws LiquibaseException, IOException, ParserConfigurationException {
Path diffFile = tempDir.resolve("diff.yaml");
DiffOutputControl diffOutputControl = new DiffOutputControl(false, false, false, null).addIncludedSchema(new CatalogAndSchema(null, null));
CommandLineUtils.doDiffToChangeLog(diffFile.toAbsolutePath().toString(), referenceDatabase, targetDatabase, null, diffOutputControl,
null, null, null, "none", "none");
if (Files.notExists(diffFile)) {
return Optional.empty();
}
return Optional.of(Files.readString(diffFile));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment