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.