執行 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
可能比稍後描述的其他替代方案更適合您的需求。請參閱 javadoc 中 ScriptUtils
的個別方法以取得更多詳細資訊。
ResourceDatabasePopulator
提供了一個基於物件的 API,用於透過外部資源中定義的 SQL 腳本,以程式化方式填充、初始化或清理資料庫。ResourceDatabasePopulator
提供了用於組態字元編碼、語句分隔符、註解分隔符,以及在解析與執行腳本時使用的錯誤處理標誌的選項。每個組態選項都有合理的預設值。請參閱 javadoc 以取得預設值的詳細資訊。若要執行 ResourceDatabasePopulator
中組態的腳本,您可以調用 populate(Connection)
方法,針對 java.sql.Connection
執行 populator,或調用 execute(DataSource)
方法,針對 javax.sql.DataSource
執行 populator。以下範例指定了測試結構描述與測試資料的 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 腳本的資源路徑,這些腳本應在整合測試類別或測試方法之前或之後針對給定的資料庫執行。SqlScriptsTestExecutionListener
提供 @Sql
的支援,預設情況下已啟用。
預設情況下,方法級別的 但是,這不適用於為 |
路徑資源語意
每個路徑都被解釋為 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 腳本或語句,則會嘗試偵測 default
腳本,具體取決於 @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_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
屬性未提供 ""
、{}
或 DEFAULT
以外的明確值時,就會繼承全域 @SqlConfig
屬性。因此,明確的本機組態會覆蓋全域組態。
@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
註解文件章節 以取得範例與更多詳細資訊。