Spring 的测试注解@Sql 如何表现得像@BeforeClass?
How can Spring's test annotation @Sql behave like @BeforeClass?
我如何才能将 @Sql
注释告诉 运行 仅一次 class,而不是每个 @Test
方法?
喜欢和 @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
}
}
您不能开箱即用。 @Sql
annotation only has two modes - BEFORE_TEST_METHOD
和 AFTER_TEST_METHOD
.
负责执行这些脚本的侦听器 SqlScriptsTestExecutionListener
未实现之前或之后的 class 方法。
为了解决这个问题,我将实现自己的 TestExecutionListener
,包装默认的 SqlScriptsTestExecutionListener
。然后您可以在测试中声明使用新的侦听器而不是旧的。
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) { }
}
你的测试将变成:
@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() { }
}
由于 DefaultTestContext.java
中的 getTestMethod()
方法,此代码抛出 IllegalStateException
(Spring 5.0.1):
public final Method getTestMethod() {
Method testMethod = this.testMethod;
Assert.state(testMethod != null, "No test method");
return testMethod;
}
当通过您建议的实现调用 beforeTestClass
方法时,textContext
不包含有效的 testMethod
(这在这个阶段是正常的):
public class BeforeClassSqlScriptsTestExecutionListener implements TestExecutionListener {
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
new SqlScriptsTestExecutionListener().beforeTestMethod(testContext);
}
}
执行 运行 SQL 脚本(在 SqlScriptsTestExecutionListener
中)的代码时,需要有效的 testMethod
:
Set<Sql> sqlAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
testContext.getTestMethod(), Sql.class, SqlGroup.class);
我最终使用了这个解决方法:
@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;
}
[...]
}
对于 JUnit 5,直接 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 ...
但是当您的数据库连接未配置 autoCommit = true
时,您必须将所有内容包装在一个事务中:
@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 ...
为什么要清理 解决方案?
因为根据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.
奖金
您可以将此方法与其他@Sql 声明混合使用。
对于 JUnit 5,我支持 adrhc 的解决方案。
对于 Junit 4,你可以这样做:
@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;
}
}
}
(同样,假设您的连接有 autoCommit=true
,请参阅 adrhc 的 post。)
如果您打算 运行 并行测试,那么您需要使方法同步。
@BeforeAll 和@BeforeClass 注释需要 'static' 方法。所以它不起作用。
配置文件中的@PostConstruct 呢?它对我很好。
@TestConfiguration
public class IntegrationTestConfiguration {
@Autowired
private DataSource dataSource;
@PostConstruct
public void initDB() throws SQLException {
try (Connection con = dataSource.getConnection()) {
ScriptUtils.executeSqlScript(con, new ClassPathResource("data.sql"));
}
}
}
@ContextConfiguration(classes = {IntegrationTestConfiguration.class})
public class YourIntegrationTest {
}
我如何才能将 @Sql
注释告诉 运行 仅一次 class,而不是每个 @Test
方法?
喜欢和 @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
}
}
您不能开箱即用。 @Sql
annotation only has two modes - BEFORE_TEST_METHOD
和 AFTER_TEST_METHOD
.
负责执行这些脚本的侦听器 SqlScriptsTestExecutionListener
未实现之前或之后的 class 方法。
为了解决这个问题,我将实现自己的 TestExecutionListener
,包装默认的 SqlScriptsTestExecutionListener
。然后您可以在测试中声明使用新的侦听器而不是旧的。
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) { }
}
你的测试将变成:
@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() { }
}
由于 DefaultTestContext.java
中的 getTestMethod()
方法,此代码抛出 IllegalStateException
(Spring 5.0.1):
public final Method getTestMethod() {
Method testMethod = this.testMethod;
Assert.state(testMethod != null, "No test method");
return testMethod;
}
当通过您建议的实现调用 beforeTestClass
方法时,textContext
不包含有效的 testMethod
(这在这个阶段是正常的):
public class BeforeClassSqlScriptsTestExecutionListener implements TestExecutionListener {
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
new SqlScriptsTestExecutionListener().beforeTestMethod(testContext);
}
}
执行 运行 SQL 脚本(在 SqlScriptsTestExecutionListener
中)的代码时,需要有效的 testMethod
:
Set<Sql> sqlAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
testContext.getTestMethod(), Sql.class, SqlGroup.class);
我最终使用了这个解决方法:
@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;
}
[...]
}
对于 JUnit 5,直接 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 ...
但是当您的数据库连接未配置 autoCommit = true
时,您必须将所有内容包装在一个事务中:
@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 ...
为什么要清理 解决方案?
因为根据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.
奖金
您可以将此方法与其他@Sql 声明混合使用。
对于 JUnit 5,我支持 adrhc 的解决方案。
对于 Junit 4,你可以这样做:
@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;
}
}
}
(同样,假设您的连接有 autoCommit=true
,请参阅 adrhc 的 post。)
如果您打算 运行 并行测试,那么您需要使方法同步。
@BeforeAll 和@BeforeClass 注释需要 'static' 方法。所以它不起作用。 配置文件中的@PostConstruct 呢?它对我很好。
@TestConfiguration
public class IntegrationTestConfiguration {
@Autowired
private DataSource dataSource;
@PostConstruct
public void initDB() throws SQLException {
try (Connection con = dataSource.getConnection()) {
ScriptUtils.executeSqlScript(con, new ClassPathResource("data.sql"));
}
}
}
@ContextConfiguration(classes = {IntegrationTestConfiguration.class})
public class YourIntegrationTest {
}