Correct post #45: Flyway and Liquibase DO run under @DataJpaTest by default
The previous commit's FlywayDoesNotRunUnderPlainDataJpaTestTest and FlywayRunsWhenExplicitlyImportedTest rested on a false premise: that none of @DataJpaTest's four documented meta-annotations import FlywayAutoConfiguration, so migrations never run under the slice. That reading of the four .imports manifests is accurate but incomplete. @AutoConfigureJdbc is itself meta-annotated with @AutoConfigureDataSourceInitialization (new in Boot 4.0), and Spring's ImportAutoConfigurationImportSelector walks the full meta-annotation tree transitively. That fifth annotation's own .imports resource is loaded via ClassLoader.getResources(), which merges same-named files from every jar on the classpath -- spring-boot-jdbc-test, spring-boot-flyway and spring-boot-liquibase each ship one at the identical path, contributing DataSourceInitializationAutoConfiguration, FlywayAutoConfiguration and LiquibaseAutoConfiguration respectively. Replaced the two wrong-premise test classes with FlywayRunsByDefaultTest (proves the bean is present and the migration actually ran) and ExplicitFlywayImportIsRedundantTest (proves the old "add the import yourself" advice is harmless but unnecessary on Boot 4.1). Added javap/unzip evidence for the new mechanism to docs/output, re-captured the full test run (9 tests, 1 clean skip), corrected README's test table, and closed a second, unrelated gap: the unzip -l listing quoted in the @ServiceConnection section had no matching captured transcript -- it does now.
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
package com.ankurm.datajpa;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Older advice for this exact situation (including an earlier draft of this module, and more
|
||||
* than one blog post this article will not link to) says to add
|
||||
* {@code @ImportAutoConfiguration(FlywayAutoConfiguration.class)} yourself to get Flyway running
|
||||
* inside a {@code @DataJpaTest}. As {@link FlywayRunsByDefaultTest} now shows, that is no longer
|
||||
* necessary on Spring Boot 4.1 -- the import already happens on its own, by way of
|
||||
* {@code @AutoConfigureJdbc}'s own {@code @AutoConfigureDataSourceInitialization} meta-annotation.
|
||||
*
|
||||
* <p>Adding the explicit import anyway is not harmful. {@code @ImportAutoConfiguration} is
|
||||
* backed by ordinary Spring auto-configuration machinery: the same fully-qualified class name,
|
||||
* however many times it is reached (once through the transitive meta-annotation walk, once
|
||||
* through this class's own explicit annotation), is only ever registered once. This test proves
|
||||
* that directly rather than asserting it from the mechanism alone: exactly one {@code flyway}
|
||||
* bean, and exactly one applied migration -- not two -- with the redundant import sitting right
|
||||
* there on the class.
|
||||
*/
|
||||
@DataJpaTest
|
||||
@ImportAutoConfiguration(FlywayAutoConfiguration.class)
|
||||
class ExplicitFlywayImportIsRedundantTest {
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext ctx;
|
||||
|
||||
@Test
|
||||
void redundantImportStillProducesExactlyOneFlywayBean() {
|
||||
assertThat(ctx.getBeanNamesForType(org.flywaydb.core.Flyway.class)).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void redundantImportDoesNotReapplyTheMigration() throws Exception {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
var rs = statement.executeQuery(
|
||||
"select count(*) from \"flyway_schema_history\" where \"installed_rank\" >= 0");
|
||||
assertThat(rs.next()).isTrue();
|
||||
int realMigrationRows = rs.getInt(1);
|
||||
System.out.println("flyway_schema_history real migration rows (rank >= 0): " + realMigrationRows);
|
||||
assertThat(realMigrationRows).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package com.ankurm.datajpa;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* This module has a real Flyway migration, {@code V1__create_product.sql}, and flyway-core is a
|
||||
* main-scope dependency. A plain @DataJpaTest never runs it anyway -- not because of the
|
||||
* embedded-database substitution, but because @DataJpaTest's own curated set of auto-configuration
|
||||
* imports simply does not include FlywayAutoConfiguration or LiquibaseAutoConfiguration at all.
|
||||
* Confirmed by reading the actual import manifests Boot ships inside spring-boot-jdbc-test.jar and
|
||||
* spring-boot-data-jpa-test.jar -- neither file mentions Flyway or Liquibase, in any form, under
|
||||
* any replace setting.
|
||||
*
|
||||
* The {@code product} table exists in this test for a completely different reason: Hibernate's own
|
||||
* {@code ddl-auto}, defaulted on because Boot never detects a schema-management tool in this
|
||||
* slice's context. See {@link FlywayRunsWhenExplicitlyImportedTest} for the one-line fix.
|
||||
*/
|
||||
@DataJpaTest
|
||||
class FlywayDoesNotRunUnderPlainDataJpaTestTest {
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Test
|
||||
void productTableExistsButFlywayNeverRan() throws Exception {
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
try (ResultSet flywayTable = connection.getMetaData()
|
||||
.getTables(null, null, "FLYWAY_SCHEMA_HISTORY", null)) {
|
||||
assertThat(flywayTable.next())
|
||||
.as("flyway_schema_history should not exist -- Flyway never ran")
|
||||
.isFalse();
|
||||
}
|
||||
try (ResultSet productTable = connection.getMetaData()
|
||||
.getTables(null, null, "PRODUCT", null)) {
|
||||
assertThat(productTable.next())
|
||||
.as("product table should exist anyway -- Hibernate's ddl-auto made it")
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ankurm.datajpa;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* This module has a real Flyway migration, {@code V1__create_product.sql}, and flyway-core
|
||||
* (plus the test-scoped {@code spring-boot-starter-flyway} -- see the module README for why that
|
||||
* second one matters) is on the classpath. A plain {@code @DataJpaTest}, with no extra
|
||||
* annotation anywhere on this class, runs that migration by default.
|
||||
*
|
||||
* <p>An earlier draft of this module asserted the opposite, on the strength of reading
|
||||
* {@code @DataJpaTest}'s four documented meta-annotations ({@code @AutoConfigureDataJpa},
|
||||
* {@code @AutoConfigureJdbc}, {@code @AutoConfigureTestDatabase},
|
||||
* {@code @AutoConfigureTestEntityManager}) and finding that none of their four
|
||||
* {@code .imports} manifests mentions Flyway or Liquibase. That reading is correct and
|
||||
* incomplete: {@code AutoConfigureJdbc.class} is itself meta-annotated with a fifth annotation,
|
||||
* {@code @AutoConfigureDataSourceInitialization} (new in Boot 4.0; confirmed with
|
||||
* {@code javap -v org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureJdbc} against
|
||||
* {@code spring-boot-jdbc-test-4.1.1.jar}), and Spring's own
|
||||
* {@code ImportAutoConfigurationImportSelector} walks the entire meta-annotation tree
|
||||
* transitively -- every annotation, and every meta-annotation on those -- collecting every class
|
||||
* in that tree that is itself annotated with {@code @ImportAutoConfiguration}. That fifth
|
||||
* annotation is one of them.
|
||||
*
|
||||
* <p>{@code @AutoConfigureDataSourceInitialization}'s own {@code .imports} resource is loaded via
|
||||
* {@code ClassLoader.getResources()}, which returns every file at that path across every jar on
|
||||
* the classpath and merges them -- not a single-file lookup. Three jars ship a file at the exact
|
||||
* same resource path
|
||||
* ({@code META-INF/spring/org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureDataSourceInitialization.imports}):
|
||||
* {@code spring-boot-jdbc-test} contributes {@code DataSourceInitializationAutoConfiguration},
|
||||
* {@code spring-boot-flyway} contributes {@code FlywayAutoConfiguration}, and
|
||||
* {@code spring-boot-liquibase} contributes {@code LiquibaseAutoConfiguration}. Confirmed by
|
||||
* unzipping both jars actually on this classpath and reading the file; the Liquibase side was
|
||||
* not independently re-run here (no Liquibase module in this repo) but the same mechanism applies
|
||||
* to it by construction. {@code FlywayAutoConfiguration} is still gated by the ordinary
|
||||
* {@code @ConditionalOnClass(Flyway.class)} -- it only activates once flyway-core is genuinely on
|
||||
* the classpath, which is the one part of the old draft's observation that was never wrong, just
|
||||
* misattributed to the wrong mechanism.
|
||||
*
|
||||
* <p>See {@link ExplicitFlywayImportIsRedundantTest} for what this means for the (now
|
||||
* unnecessary, but harmless) advice to {@code @ImportAutoConfiguration(FlywayAutoConfiguration.class)}
|
||||
* yourself.
|
||||
*/
|
||||
@DataJpaTest
|
||||
class FlywayRunsByDefaultTest {
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext ctx;
|
||||
|
||||
@Test
|
||||
void flywayAutoConfigurationBeansArePresentWithNoExplicitImportAnywhere() {
|
||||
assertThat(ctx.getBeanNamesForType(org.flywaydb.core.Flyway.class)).isNotEmpty();
|
||||
assertThat(ctx.containsBean("flywayInitializer")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void theMigrationActuallyRanAgainstTheSubstitutedDatabase() throws Exception {
|
||||
// Flyway's own history table always carries a bookkeeping row at installed_rank = -1
|
||||
// ("<< Flyway Schema History table created >>") ahead of the real migration rows, and
|
||||
// both the table name and every column name are stored quoted and lowercase -- H2's
|
||||
// default unquoted-identifier folding (to uppercase) does not apply to any of them.
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement();
|
||||
ResultSet rs = statement.executeQuery(
|
||||
"select \"installed_rank\", \"version\", \"description\", \"success\" "
|
||||
+ "from \"flyway_schema_history\" where \"installed_rank\" = 1")) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
String version = rs.getString(2);
|
||||
String description = rs.getString(3);
|
||||
boolean success = rs.getBoolean(4);
|
||||
System.out.println("flyway_schema_history row: version=" + version
|
||||
+ " description=" + description + " success=" + success);
|
||||
assertThat(version).isEqualTo("1");
|
||||
assertThat(description).isEqualTo("create product");
|
||||
assertThat(success).isTrue();
|
||||
assertThat(rs.next()).isFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package com.ankurm.datajpa;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* The one-line fix for {@link FlywayDoesNotRunUnderPlainDataJpaTestTest}: pull
|
||||
* FlywayAutoConfiguration into the slice yourself. Once it is in the context, Boot's own
|
||||
* schema-management detection sees it, flips Hibernate's ddl-auto back to {@code none}, and the
|
||||
* real migration runs against the same substituted (here, H2) DataSource.
|
||||
*/
|
||||
@DataJpaTest
|
||||
@ImportAutoConfiguration(FlywayAutoConfiguration.class)
|
||||
class FlywayRunsWhenExplicitlyImportedTest {
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
|
||||
@Test
|
||||
void flywayHistoryTableProvesTheMigrationRanThisTime() throws Exception {
|
||||
// Flyway's own history table always carries a bookkeeping row at installed_rank = -1
|
||||
// ("<< Flyway Schema History table created >>") ahead of the real migration rows --
|
||||
// easy to miss if a query only reads the first row back, which is what an earlier draft
|
||||
// of this test did. The real V1 migration is installed_rank = 1.
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement();
|
||||
ResultSet rs = statement.executeQuery(
|
||||
"select \"installed_rank\", \"version\", \"description\", \"success\" "
|
||||
+ "from \"flyway_schema_history\" where \"installed_rank\" = 1")) {
|
||||
assertThat(rs.next()).isTrue();
|
||||
String version = rs.getString(2);
|
||||
String description = rs.getString(3);
|
||||
boolean success = rs.getBoolean(4);
|
||||
System.out.println("flyway_schema_history row: version=" + version
|
||||
+ " description=" + description + " success=" + success);
|
||||
assertThat(version).isEqualTo("1");
|
||||
assertThat(description).isEqualTo("create product");
|
||||
assertThat(success).isTrue();
|
||||
assertThat(rs.next()).isFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user