How can Spring's test annotation @Sql behave like @BeforeClass?
You can't do that out-of-the-box. The @Sql
annotation only has two modes - BEFORE_TEST_METHOD
and AFTER_TEST_METHOD
.
The listener responsible for executing these scripts, SqlScriptsTestExecutionListener
, does not implement before or after-class methods.
To work around this, I'd implement my own TestExecutionListener
, wrapping the default SqlScriptsTestExecutionListener
. You can then declare on your test to use the new listener rather than the old ones.
public class BeforeClassSqlScriptsTestExecutionListener implements TestExecutionListener
{
@Override
public void beforeTestClass(final TestContext testContext) throws Exception
{
// Note, we're deliberately calling beforeTest*Method*
new SqlScriptsTestExecutionListener().beforeTestMethod(testContext);
}
@Override
public void prepareTestInstance(final TestContext testContext) { }
@Override
public void beforeTestMethod(final TestContext testContext) { }
@Override
public void afterTestMethod(final TestContext testContext) { }
@Override
public void afterTestClass(final TestContext testContext) { }
}
Your test would then become:
@TestExecutionListeners(
listeners = { BeforeClassSqlScriptsTestExecutionListener.class },
/* Here, we're replacing more than just SqlScriptsTestExecutionListener, so manually
include any of the default above if they're still needed: */
mergeMode = TestExecutionListeners.MergeMode.REPLACE_DEFAULTS
)
@org.springframework.test.context.jdbc.Sql(
scripts = "classpath:schema-test.sql",
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD
)
public class MyTest
{
@Test
public void test1() { }
@Test
public void test2() { }
}
For JUnit 5, the straight forward clean solution:
@MyInMemoryDbConfig
//@Sql(value = {"/appconfig.sql", "/album.sql"}) -> code below is equivalent but at class level
class SomeServiceTest {
@BeforeAll
void setup(@Autowired DataSource dataSource) {
try (Connection conn = dataSource.getConnection()) {
// you'll have to make sure conn.autoCommit = true (default for e.g. H2)
// e.g. url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1;MODE=MySQL
ScriptUtils.executeSqlScript(conn, new ClassPathResource("appconfig.sql"));
ScriptUtils.executeSqlScript(conn, new ClassPathResource("album.sql"));
}
}
// your @Test methods follow ...
but when your database connections are not configured with autoCommit = true
you'll have to wrap all in a transaction:
@RootInMemoryDbConfig
@Slf4j
class SomeServiceTest {
@BeforeAll
void setup(@Autowired DataSource dataSource,
@Autowired PlatformTransactionManager transactionManager) {
new TransactionTemplate(transactionManager).execute((ts) -> {
try (Connection conn = dataSource.getConnection()) {
ScriptUtils.executeSqlScript(conn, new ClassPathResource("appconfig.sql"));
ScriptUtils.executeSqlScript(conn, new ClassPathResource("album.sql"));
// should work without manually commit but didn't for me (because of using AUTOCOMMIT=OFF)
// I use url=jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1;MODE=MySQL;AUTOCOMMIT=OFF
// same will happen with DataSourceInitializer & DatabasePopulator (at least with this setup)
conn.commit();
} catch (SQLException e) {
SomeServiceTest.log.error(e.getMessage(), e);
}
return null;
});
}
// your @Test methods follow ...
Why clean solution?
Because according to Script Configuration with @SqlConfig:
The configuration options provided by @Sql and @SqlConfig are equivalent to those supported by ScriptUtils and ResourceDatabasePopulator but are a superset of those provided by the XML namespace element.
Bonus
You can mix this approach with other @Sql declarations.