How can Spring's test annotation @Sql behave like @BeforeClass?

核能气质少年 提交于 2020-12-02 07:10:11

问题


How can I tell the @Sql annotation to run only once for the class, and not for each @Test method?

Like having the same behaviour as @BeforeClass?

@org.springframework.test.context.jdbc.Sql(
     scripts = "classpath:schema-test.sql",
     executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD
)
public class TestClass {
      @Test
      public void test1() {
        //runs the @Sql script
      }

      @Test
      public void test2() {
        //runs the @Sql script again
      }
}

回答1:


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() { }
}



回答2:


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.




回答3:


For JUnit 5 I second the solution by adrhc.

For Junit 4, you can do:

@Autowired
private DataSource database;

private static boolean dataLoaded = false;

    @Before
    public void setup() throws SQLException {
        if(!dataLoaded) {
            try (Connection con = database.getConnection()) {
                ScriptUtils.executeSqlScript(con, new ClassPathResource("path/to/script.sql"));
                dataLoaded = true;
            }
        }
    }

(Again, assuming your connection has autoCommit=true, see post by adrhc.)

If you intend to run your tests in parallel then you'll need to replace the boolean with an AtomicBoolean.




回答4:


This code throws an IllegalStateException (Spring 5.0.1) because of the getTestMethod() method in DefaultTestContext.java:

public final Method getTestMethod() {
    Method testMethod = this.testMethod;
    Assert.state(testMethod != null, "No test method");
    return testMethod;
}

When calling the beforeTestClass method through your proposed implementation, the textContext does not contain a valid testMethod (which is normal at this stage):

public class BeforeClassSqlScriptsTestExecutionListener implements TestExecutionListener {

    @Override
    public void beforeTestClass(TestContext testContext) throws Exception {
        new SqlScriptsTestExecutionListener().beforeTestMethod(testContext);
    }
}

When the code responsible of running SQL scripts (in the SqlScriptsTestExecutionListener) is executed, a valid testMethod is required:

Set<Sql> sqlAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
            testContext.getTestMethod(), Sql.class, SqlGroup.class);

I ended up using this workaround:

@Before
public void setUp() {
    // Manually initialize DB as @Sql annotation doesn't support class-level execution phase (actually executed before every test method)
    // See https://jira.spring.io/browse/SPR-14357
    if (!dbInitialized) {
        final ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator();
        resourceDatabasePopulator.addScript(new ClassPathResource("/sql/[...].sql"));
        resourceDatabasePopulator.execute(dataSource);
        dbInitialized = true;
    }
    [...]
}


来源:https://stackoverflow.com/questions/47775560/how-can-springs-test-annotation-sql-behave-like-beforeclass

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!