|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
执行 SQL 脚本
在针对关系型数据库编写集成测试时,通常需要运行 SQL 脚本来修改数据库结构或向表中插入测试数据。spring-jdbc 模块提供了对嵌入式或现有数据库进行初始化的支持,即在 Spring ApplicationContext 加载时执行 SQL 脚本。详情请参见
嵌入式数据库支持 和
使用嵌入式数据库测试数据访问逻辑。
尽管在加载ApplicationContext时一次性初始化数据库用于测试非常有用,但有时在集成测试期间能够修改数据库至关重要。以下章节将解释如何在集成测试期间以编程方式和声明方式运行 SQL 脚本。
以编程方式执行 SQL 脚本
Spring 提供了以下选项,用于在集成测试方法中以编程方式执行 SQL 脚本。
-
org.springframework.jdbc.datasource.init.ScriptUtils -
org.springframework.jdbc.datasource.init.ResourceDatabasePopulator -
org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests -
org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
ScriptUtils 提供了一组用于处理 SQL 脚本的静态工具方法,主要用于框架内部使用。然而,如果您需要完全控制 SQL 脚本的解析和执行方式,ScriptUtils 可能比后面描述的一些其他替代方案更适合您的需求。有关 https://docs.spring.io/spring-framework/docs/6.0.23/javadoc-api/org/springframework/jdbc/datasource/init/ScriptUtils.html 中各个方法的更多详细信息,请参阅javadoc。
ResourceDatabasePopulator 提供了一个基于对象的 API,用于通过使用定义在外部资源中的 SQL 脚本来以编程方式填充、初始化或清理数据库。ResourceDatabasePopulator 提供了配置字符编码、语句分隔符、注释分隔符以及解析和运行脚本时使用的错误处理标志的选项。每个配置选项都有一个合理的默认值。有关默认值的详细信息,请参阅 javadoc。要运行在 ResourceDatabasePopulator 中配置的脚本,您可以调用 populate(Connection) 方法针对 java.sql.Connection 运行填充器,或者调用 execute(DataSource) 方法针对 javax.sql.DataSource 运行填充器。以下示例指定了用于测试模式和测试数据的 SQL 脚本,将语句分隔符设置为 @@,并针对 DataSource 运行这些脚本:
-
Java
-
Kotlin
@Test
void databaseTest() {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScripts(
new ClassPathResource("test-schema.sql"),
new ClassPathResource("test-data.sql"));
populator.setSeparator("@@");
populator.execute(this.dataSource);
// run code that uses the test schema and data
}
@Test
fun databaseTest() {
val populator = ResourceDatabasePopulator()
populator.addScripts(
ClassPathResource("test-schema.sql"),
ClassPathResource("test-data.sql"))
populator.setSeparator("@@")
populator.execute(dataSource)
// run code that uses the test schema and data
}
请注意,ResourceDatabasePopulator 在内部会委托 ScriptUtils 来解析并执行 SQL 脚本。类似地,
AbstractTransactionalJUnit4SpringContextTests
和 AbstractTransactionalTestNGSpringContextTests
中的 executeSqlScript(..) 方法在内部使用 ResourceDatabasePopulator 来执行 SQL 脚本。有关更多详情,请参阅各种 executeSqlScript(..) 方法的 Javadoc。
使用 @Sql 声明式地执行 SQL 脚本
除了上述以编程方式运行 SQL 脚本的机制之外,你还可以在 Spring TestContext 框架中以声明式方式配置 SQL 脚本。
具体来说,你可以在测试类或测试方法上声明 @Sql 注解,
以配置应在集成测试方法执行之前或之后针对指定数据库运行的单条 SQL 语句或 SQL 脚本的资源路径。
@Sql 的支持由 SqlScriptsTestExecutionListener 提供,该监听器默认已启用。
方法级别的 @Sql 声明默认会覆盖类级别的声明。然而,从 Spring Framework 5.2 开始,可以通过 @SqlMergeMode 按测试类或按测试方法配置此行为。有关更多详细信息,请参阅 使用 @SqlMergeMode 合并和覆盖配置。 |
路径资源语义
每个路径都被解释为一个 Spring Resource。一个普通路径(例如,"schema.sql")被视为相对于测试类所在包的 classpath 资源。以斜杠开头的路径被视为绝对 classpath 资源(例如,"/org/example/schema.sql")。引用 URL 的路径(例如,以 classpath:、file:、http: 为前缀的路径)将使用指定的资源协议进行加载。
以下示例展示了如何在基于 JUnit Jupiter 的集成测试类中,在类级别和方法级别使用 @Sql:
-
Java
-
Kotlin
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
@Test
void emptySchemaTest() {
// run code that uses the test schema without any test data
}
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})
void userTest() {
// run code that uses the test schema and test data
}
}
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
@Test
fun emptySchemaTest() {
// run code that uses the test schema without any test data
}
@Test
@Sql("/test-schema.sql", "/test-user-data.sql")
fun userTest() {
// run code that uses the test schema and test data
}
}
默认脚本检测
如果没有指定 SQL 脚本或语句,系统会尝试根据 default 注解的声明位置自动检测一个 @Sql 脚本。如果无法检测到默认脚本,则会抛出一个 IllegalStateException 异常。
-
类级别的声明:如果被注解的测试类是
com.example.MyTest,则对应的默认脚本为classpath:com/example/MyTest.sql。 -
方法级别的声明:如果带注解的测试方法名为
testMethod(),并且定义在类com.example.MyTest中,则对应的默认脚本为classpath:com/example/MyTest.testMethod.sql。
声明多个@Sql集合
如果你需要为某个测试类或测试方法配置多组 SQL 脚本,且每组脚本具有不同的语法配置、不同的错误处理规则或不同的执行阶段,你可以声明多个 @Sql 实例。在 Java 8 中,你可以将 @Sql 用作可重复注解。否则,你可以使用 @SqlGroup 注解作为显式容器,用于声明多个 @Sql 实例。
以下示例展示了如何在 Java 8 中将 @Sql 作为可重复注解使用:
-
Java
-
Kotlin
@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
// run code that uses the test schema and test data
}
// Repeatable annotations with non-SOURCE retention are not yet supported by Kotlin
在前面示例所展示的场景中,test-schema.sql 脚本使用了不同的单行注释语法。
以下示例与前面的示例完全相同,只是 @Sql 声明被组合在 @SqlGroup 中。在 Java 8 及更高版本中,使用 @SqlGroup 是可选的,但为了与其他 JVM 语言(例如 Kotlin)兼容,您可能需要使用 @SqlGroup。
-
Java
-
Kotlin
@Test
@SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
)}
void userTest() {
// run code that uses the test schema and test data
}
@Test
@SqlGroup(
Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
Sql("/test-user-data.sql"))
fun userTest() {
// Run code that uses the test schema and test data
}
脚本执行阶段
默认情况下,SQL 脚本会在对应的测试方法之前执行。然而,如果你需要在测试方法之后运行特定的一组脚本(例如,清理数据库状态),你可以在 executionPhase 注解中使用 @Sql 属性,如下例所示:
-
Java
-
Kotlin
@Test
@Sql(
scripts = "create-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
scripts = "delete-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD
)
void userTest() {
// run code that needs the test data to be committed
// to the database outside of the test's transaction
}
@Test
@SqlGroup(
Sql("create-test-data.sql",
config = SqlConfig(transactionMode = ISOLATED)),
Sql("delete-test-data.sql",
config = SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD))
fun userTest() {
// run code that needs the test data to be committed
// to the database outside of the test's transaction
}
请注意,ISOLATED 和 AFTER_TEST_METHOD 分别是从 Sql.TransactionMode 和 Sql.ExecutionPhase 静态导入的。
使用 进行脚本配置@SqlConfig
你可以使用 @SqlConfig 注解来配置脚本解析和错误处理。当在一个集成测试类中以类级别注解的形式声明时,@SqlConfig 作为该测试类层次结构中所有 SQL 脚本的全局配置。当使用@2注解的@Sql属性直接声明时,@SqlConfig作为嵌套在@Sql注解内的SQL脚本的局部配置。每个在@SqlConfig中的属性都有一个隐含的默认值,该默认值在相应属性的javadoc中进行了文档说明。由于 Java 语言规范中为注解属性定义的规则,很不幸的是,无法将值null分配给注解属性。因此,为了支持对继承的全局配置进行覆盖,@SqlConfig 属性具有显式的默认值,分别为 ""(用于字符串)、{}(用于数组)或 DEFAULT(用于枚举)。这种方法允许在局部声明@SqlConfig中选择性地覆盖来自全局声明@SqlConfig的个别属性,通过提供不同于""、{}或DEFAULT的值。全局@SqlConfig属性会在局部@SqlConfig属性未提供除""、{}或
DEFAULT以外的显式值时被继承。因此,显式的本地配置会覆盖全局配置。
@Sql 和 @SqlConfig 提供的配置选项等同于 ScriptUtils 和 ResourceDatabasePopulator 所支持的选项,但它们是 <jdbc:initialize-database/> XML 命名空间元素所提供选项的超集。有关详细信息,请参阅 @Sql 和 @SqlConfig 中各个属性的 Javadoc。
@Sql 的事务管理
默认情况下,SqlScriptsTestExecutionListener 会推断通过 @Sql 配置的脚本所需的事务语义。具体而言,SQL 脚本会在无事务、现有 Spring 管理的事务(例如,由针对使用 @Transactional 注解的测试所管理的 TransactionalTestExecutionListener 事务)或隔离事务中运行,这取决于 @SqlConfig 中 transactionMode 属性的配置值以及测试的 ApplicationContext 中是否存在 PlatformTransactionManager。然而,最起码的要求是测试的 ApplicationContext 中必须存在 javax.sql.DataSource。
如果 SqlScriptsTestExecutionListener 用于检测 DataSource 和
PlatformTransactionManager 并推断事务语义的算法不符合您的需求,
您可以通过设置 @SqlConfig 的 dataSource 和 transactionManager
属性来指定显式名称。此外,您可以通过设置 @SqlConfig 的 transactionMode 属性来控制事务传播行为(例如,脚本是否应在隔离的事务中运行)。虽然对本参考手册而言,全面讨论使用 @Sql 进行事务管理的所有支持选项超出了范围,但
@SqlConfig 和
SqlScriptsTestExecutionListener 的 Javadoc 提供了详细信息,以下示例展示了一个使用 JUnit Jupiter 和带有 @Sql 的事务性测试的典型测试场景:
-
Java
-
Kotlin
@SpringJUnitConfig(TestDatabaseConfig.class)
@Transactional
class TransactionalSqlScriptsTests {
final JdbcTemplate jdbcTemplate;
@Autowired
TransactionalSqlScriptsTests(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Test
@Sql("/test-data.sql")
void usersTest() {
// verify state in test database:
assertNumUsers(2);
// run code that uses the test data...
}
int countRowsInTable(String tableName) {
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
}
void assertNumUsers(int expected) {
assertEquals(expected, countRowsInTable("user"),
"Number of rows in the [user] table.");
}
}
@SpringJUnitConfig(TestDatabaseConfig::class)
@Transactional
class TransactionalSqlScriptsTests @Autowired constructor(dataSource: DataSource) {
val jdbcTemplate: JdbcTemplate = JdbcTemplate(dataSource)
@Test
@Sql("/test-data.sql")
fun usersTest() {
// verify state in test database:
assertNumUsers(2)
// run code that uses the test data...
}
fun countRowsInTable(tableName: String): Int {
return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName)
}
fun assertNumUsers(expected: Int) {
assertEquals(expected, countRowsInTable("user"),
"Number of rows in the [user] table.")
}
}
请注意,在 usersTest() 方法执行后,无需清理数据库,因为对数据库所做的任何更改(无论是测试方法内部还是在 /test-data.sql 脚本中进行的更改)都会被 TransactionalTestExecutionListener 自动回滚(详情请参见事务管理)。
使用 合并和覆盖配置@SqlMergeMode
自 Spring Framework 5.2 起,可以将方法级别的 @Sql 声明与类级别的声明进行合并。例如,这允许您为每个测试类提供一次数据库模式或某些通用测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。要启用 @Sql 合并,请使用 @SqlMergeMode(MERGE) 注解您的测试类或测试方法。若要禁用特定测试方法(或特定测试子类)的合并,您可以通过 @SqlMergeMode(OVERRIDE) 切换回默认模式。请参阅 @SqlMergeMode 注解文档部分 以获取示例和更多详细信息。