执行 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
可能比后面描述的其他一些替代方案更适合您的需求。有关 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
的 Environment
中存储的属性替换。
下面的示例展示了如何在基于 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
的声明位置检测一个 default
脚本。如果无法检测到默认脚本,则会抛出 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_METHOD
分别从 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
是从 Sql.ExecutionPhase
中静态导入的。
使用 @SqlConfig
进行脚本配置
您可以使用 @SqlConfig
注解配置脚本解析和错误处理。当在集成测试类上声明为类级别注解时,@SqlConfig
作为测试类层次结构中所有 SQL 脚本的全局配置。当通过 @Sql
注解的 config
属性直接声明时,@SqlConfig
作为封闭 @Sql
注解中声明的 SQL 脚本的局部配置。@SqlConfig
中的每个属性都有一个隐式默认值,这在相应属性的 javadoc 中有记录。由于 Java 语言规范中定义的注解属性规则,遗憾的是无法为注解属性分配 null
值。因此,为了支持覆盖继承的全局配置,@SqlConfig
属性具有显式默认值,分别为 ""
(对于字符串)、{}
(对于数组)或 DEFAULT
(对于枚举)。这种方法允许 @SqlConfig
的局部声明通过提供 ""
、{}
或 DEFAULT
以外的值来选择性地覆盖 @SqlConfig
的全局声明中的各个属性。全局 @SqlConfig
属性在局部 @SqlConfig
属性未提供 ""
、{}
或 DEFAULT
以外的显式值时被继承。因此,显式的局部配置会覆盖全局配置。
@Sql
和 @SqlConfig
提供的配置选项与 ScriptUtils
和 ResourceDatabasePopulator
支持的选项相同,但它们是 <jdbc:initialize-database/>
XML 命名空间元素提供的选项的超集。有关详细信息,请参阅 @Sql 和 @SqlConfig 中各个属性的 javadoc。
@Sql
的事务管理
默认情况下,SqlScriptsTestExecutionListener
会推断使用 @Sql
配置的脚本所需的事务语义。具体来说,SQL 脚本可以在没有事务的情况下运行,也可以在现有的 Spring 管理的事务中运行(例如,由 TransactionalTestExecutionListener
管理的事务,针对使用 @Transactional
注解的测试),或者在一个独立的事务中运行,这取决于 @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
可以将方法级别的 @Sql
声明与类级别的声明合并。例如,这允许您为数据库模式或某些常见的测试数据提供一次配置,然后为每个测试方法提供额外的、特定用例的测试数据。要启用 @Sql
合并,可以在测试类或测试方法上注解 @SqlMergeMode(MERGE)
。要为特定测试方法(或特定测试子类)禁用合并,可以通过 @SqlMergeMode(OVERRIDE)
切换回默认模式。有关示例和更多详细信息,请查阅 @SqlMergeMode 注解文档部分。