執行 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
執行 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
在內部將 SQL 指令碼的解析和執行委託給 ScriptUtils
。類似地,AbstractTransactionalJUnit4SpringContextTests
和 AbstractTransactionalTestNGSpringContextTests
中的 executeSqlScript(..)
方法在內部使用 ResourceDatabasePopulator
來執行 SQL 指令碼。詳見各種 executeSqlScript(..)
方法的 Javadoc。
使用 @Sql 宣告式執行 SQL 指令碼
除了上述以程式設計方式執行 SQL 指令碼的機制之外,您還可以在 Spring TestContext 框架中宣告式地配置 SQL 指令碼。具體來說,您可以在測試類或測試方法上宣告 @Sql
註解,以配置在整合測試類或測試方法之前或之後針對給定資料庫執行的單個 SQL 語句或 SQL 指令碼的資源路徑。對 @Sql
的支援由 SqlScriptsTestExecutionListener
提供,該監聽器預設是啟用的。
方法級別的 但是,這不適用於為 |
路徑資源語義
每個路徑都被解釋為一個 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
屬性具有顯式的預設值,可以是 ""
(對於 String)、{}
(對於陣列)或 DEFAULT
(對於列舉)。這種方法允許本地 @SqlConfig
宣告透過提供不同於 ""
、{}
或 DEFAULT
的值來選擇性地覆蓋全域性 @SqlConfig
宣告中的單個屬性。當本地 @SqlConfig
屬性沒有提供顯式值(不同於 ""
、{}
或 DEFAULT
)時,全域性 @SqlConfig
屬性會被繼承。因此,顯式的本地配置會覆蓋全域性配置。
@Sql
和 @SqlConfig
提供的配置選項與 ScriptUtils
和 ResourceDatabasePopulator
支援的選項等價,但它們是 <jdbc:initialize-database/>
XML 名稱空間元素提供的選項的超集。詳見 @Sql
和 @SqlConfig
中各個屬性的 javadoc。
@Sql
的事務管理
預設情況下,SqlScriptsTestExecutionListener
會推斷使用 @Sql
配置的指令碼所需的事務語義。具體來說,SQL 指令碼可以在無事務、現有 Spring 管理的事務內(例如,由為 @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
註解文件部分。