执行SQL脚本
在针对关系型数据库编写集成测试时,通常运行SQL脚本来修改数据库模式或将测试数据插入表中是很有帮助的。spring-jdbc模块提供了在加载Spring ApplicationContext时通过执行SQL脚本来初始化嵌入式或现有数据库的支持。详情请参见嵌入式数据库支持和使用嵌入式数据库测试数据访问逻辑。
尽管在ApplicationContext加载时初始化数据库以进行一次测试非常有用,但有时在集成测试期间能够修改数据库是必不可少的。以下部分将解释如何在集成测试期间以编程方式和声明式方式运行SQL脚本。
以编程方式执行SQL脚本
Spring在集成测试方法中提供了以下几种程序化执行SQL脚本的选项。
org.springframework.jdbc.datasource.init.ScriptUtilsorg.springframework.jdbc.datasource.init.ResourceDatabasePopulatororg.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTestsorg.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
ScriptUtils 提供了一组用于处理 SQL 脚本的静态实用方法,主要用于框架内部的用途。然而,如果您需要完全控制 SQL 脚本的解析和运行方式,那么 ScriptUtils 可能比后面描述的其他选项更符合您的需求。有关 ScriptUtils 中各个方法的更多详细信息,请参阅 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 声明默认会覆盖类级别的声明,但可以通过 @SqlMergeMode 按测试类或测试方法进行配置。有关更多详细信息,请参阅 使用 @SqlMergeMode 合并和覆盖配置。
不过,这不适用于为 BEFORE_TEST_CLASS 或 AFTER_TEST_CLASS 执行阶段配置的类级别声明。此类声明不能被覆盖,相应的脚本和语句将针对每个类执行一次,此外还会执行任何方法级别的脚本和语句。
路径资源语义
每条路径都被解释为Spring的Resource。一个普通的路径(例如,"schema.sql")被视为相对于测试类定义所在包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如,"/org/example/schema.sql")。引用URL的路径(例如,以classpath:、file:、http:为前缀的路径)会使用指定的资源协议进行加载。
从 Spring Framework 6.2 开始,路径中可以包含属性占位符(${…}),这些占位符将被测试的 ApplicationContext 中存储的属性所替换。
以下示例展示了如何在基于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脚本或语句,系统会尝试检测一个“默认”脚本,具体取决于@Sql的声明位置。如果无法检测到默认脚本,则会抛出IllegalStateException异常。
-
类级别声明:如果带注释的测试类的名称为
com.example.MyTest,则对应的默认脚本文件名为classpath:com/example/MyTest.sql。 -
方法级别声明:如果带注释的测试方法的名称为
testMethod(),并且该方法定义在类com.example.MyTest中,那么对应的默认脚本文件名为classpath:com/example/MyTest.testMethod.sql。
记录SQL脚本和语句
如果你想查看正在执行的SQL脚本,请将org.springframework.test.context.jdbc的日志级别设置为DEBUG。
如果你想查看哪些SQL语句正在被执行,将org.springframework.jdbc.datasource.init日志级别设置为DEBUG。
声明多个 @Sql 注解
如果你需要为给定的测试类或测试方法配置多组SQL脚本,但这些脚本具有不同的语法配置、不同的错误处理规则,或者每组脚本有不同的执行阶段,你可以声明多个@Sql实例。你可以将@Sql用作可重复使用的注解,也可以使用@SqlGroup注解作为显式的容器来声明多个@Sql实例。
以下示例展示了如何将@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
}
@Test
@Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
fun userTest() {
// run code that uses the test schema and test data
}
在前面的例子中提到的场景中,test-schema.sql 脚本使用了一种不同的单行注释语法。
以下示例与前面的示例完全相同,不同之处在于@Sql声明被组合在@SqlGroup内部。使用@SqlGroup是可选的,但为了与其他JVM语言兼容,你可能需要使用@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脚本会在相应的测试方法之前运行。但是,如果您需要在测试方法之后运行一组特定的脚本(例如,为了清理数据库状态),您可以将@Sql中的executionPhase属性设置为AFTER_TEST_METHOD,如下例所示:
- 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
@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 方法是从 Sql.TransactionMode和Sql.ExecutionPhase` 中静态导入的。
从 Spring Framework 6.1 开始,可以通过在类级别的 @Sql 声明中设置 executionPhase 属性为 BEFORE_TEST_CLASS 或 AFTER_TEST_CLASS 来在测试类之前或之后运行特定的脚本集,如下例所示:
- Java
- Kotlin
@SpringJUnitConfig
@Sql(scripts = "/test-schema.sql", executionPhase = BEFORE_TEST_CLASS)
class DatabaseTests {
@Test
void emptySchemaTest() {
// run code that uses the test schema without any test data
}
@Test
@Sql("/test-user-data.sql")
void userTest() {
// run code that uses the test schema and test data
}
}
@SpringJUnitConfig
@Sql("/test-schema.sql", executionPhase = BEFORE_TEST_CLASS)
class DatabaseTests {
@Test
fun emptySchemaTest() {
// run code that uses the test schema without any test data
}
@Test
@Sql("/test-user-data.sql")
fun userTest() {
// run code that uses the test schema and test data
}
}
BEFORE_TEST_CLASS是从SqlExecutionPhase中静态导入的。
使用 @SqlConfig 进行脚本配置
你可以通过使用@SqlConfig注解来配置脚本解析和错误处理。当在集成测试类上将其声明为类级注解时,@SqlConfig将作为该测试类层次结构内所有SQL脚本的全局配置。而当直接通过@Sql注解的config属性进行声明时,@SqlConfig则作为该@Sql注解内部声明的SQL脚本的局部配置。@SqlConfig中的每个属性都有一个隐式的默认值,该默认值在相应的Java文档中有说明。由于Java语言规范中对注解属性的定义规则,遗憾的是,无法将null值赋给注解属性。因此,为了支持对继承来的全局配置进行覆盖,@SqlConfig属性有明确的默认值:对于字符串类型为"",对于数组类型为{},对于枚举类型为DEFAULT。通过提供不同于""、{}或DEFAULT的值,这种方式允许局部声明的@SqlConfig选择性地覆盖全局声明的@SqlConfig中的个别属性。因此,当局部@SqlConfig属性没有提供除了""、{}或DEFAULT之外的其他值时,就会继承全局@SqlConfig属性的设置。由此可见,显式的局部配置会覆盖全局配置。
@Sql 和 @SqlConfig 提供的配置选项与 ScriptUtils 和 ResourceDatabasePopulator 支持的配置选项相当,但它们也是 <jdbc:initialize-database/> XML 命名空间元素所提供配置选项的子集。有关详细信息,请参阅 @Sql 和 @SqlConfig 中各个属性的 Java 文档。
@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](https://docs.spring.io/spring-framework/docs/7.0.3/javadoc-api/org/springframework/test/context/jdbc/Sql ScriptsTestExecutionListener.html)的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 合并和覆盖配置
可以将方法级别的@Sql声明与类级别的声明合并。例如,这样你就可以为每个测试类提供一次数据库模式的配置或一些通用的测试数据,然后为每个测试方法提供额外的、特定用例的测试数据。要启用@Sql合并功能,可以在测试类或测试方法上添加@SqlMergeMode(MERGE)注解。如果想取消对特定测试方法(或特定测试子类)的合并功能,可以通过@SqlMergeMode(OVERRIDE)注解切换回默认模式。有关示例和更多详细信息,请参阅@SqlMergeMode注解文档部分。