執行 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 可能比稍後描述的其他替代方案更適合您的需求。請參閱 javadocScriptUtils 的個別方法以取得更多詳細資訊。

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 腳本。同樣地,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests 中的 executeSqlScript(..) 方法也在內部使用 ResourceDatabasePopulator 來執行 SQL 腳本。請參閱各種 executeSqlScript(..) 方法的 Javadoc 以取得更多詳細資訊。

使用 @Sql 宣告式執行 SQL 腳本

除了上述用於以程式化方式執行 SQL 腳本的機制外,您還可以在 Spring TestContext 框架中宣告式地組態 SQL 腳本。具體來說,您可以在測試類別或測試方法上宣告 @Sql 註解,以組態個別的 SQL 語句或 SQL 腳本的資源路徑,這些腳本應在整合測試類別或測試方法之前或之後針對給定的資料庫執行。SqlScriptsTestExecutionListener 提供 @Sql 的支援,預設情況下已啟用。

預設情況下,方法級別的 @Sql 宣告會覆蓋類別級別的宣告,但此行為可以透過 @SqlMergeMode 針對每個測試類別或每個測試方法進行組態。請參閱 使用 @SqlMergeMode 合併與覆蓋組態 以取得更多詳細資訊。

但是,這不適用於為 BEFORE_TEST_CLASSAFTER_TEST_CLASS 執行階段組態的類別級別宣告。此類宣告無法覆蓋,且對應的腳本與語句將針對每個類別執行一次,此外還會執行任何方法級別的腳本與語句。

路徑資源語意

每個路徑都被解釋為 Spring Resource。純路徑(例如,"schema.sql")被視為相對於測試類別定義所在套件的類路徑資源。以斜線開頭的路徑被視為絕對類路徑資源(例如,"/org/example/schema.sql")。引用 URL 的路徑(例如,以 classpath:file:http: 為前綴的路徑)會使用指定的資源協定載入。

從 Spring Framework 6.2 開始,路徑可以包含屬性佔位符 (${…​}),這些佔位符將被測試 ApplicationContextEnvironment 中儲存的屬性替換。

以下範例展示如何在基於 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
}
ISOLATEDAFTER_TEST_METHOD 是從 Sql.TransactionModeSql.ExecutionPhase 靜態導入的。

從 Spring Framework 6.1 開始,可以透過將類別級別 @Sql 宣告中的 executionPhase 屬性設定為 BEFORE_TEST_CLASSAFTER_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 提供的組態選項等同於 ScriptUtilsResourceDatabasePopulator 支援的選項,但超出了 <jdbc:initialize-database/> XML 命名空間元素提供的選項的超集。請參閱 @Sql@SqlConfig 中個別屬性的 javadoc 以取得詳細資訊。

@Sql 的交易管理

預設情況下,SqlScriptsTestExecutionListener 會推斷使用 @Sql 組態的腳本所需的交易語意。具體來說,SQL 腳本在沒有交易的情況下執行,在現有的 Spring 管理的交易中執行(例如,由 TransactionalTestExecutionListener 為使用 @Transactional 註解的測試管理的交易),或在隔離的交易中執行,具體取決於 @SqlConfigtransactionMode 屬性的組態值以及測試 ApplicationContext 中是否存在 PlatformTransactionManager。但是,最基本的要求是,測試 ApplicationContext 中必須存在 javax.sql.DataSource

如果 SqlScriptsTestExecutionListener 用於偵測 DataSourcePlatformTransactionManager 並推斷交易語意的演算法不符合您的需求,您可以透過設定 @SqlConfigdataSourcetransactionManager 屬性來指定明確的名稱。此外,您可以透過設定 @SqlConfigtransactionMode 屬性來控制交易傳播行為(例如,腳本是否應在隔離的交易中執行)。雖然對 @Sql 的所有支援交易管理選項進行詳盡的討論超出了本參考手冊的範圍,但 @SqlConfigSqlScriptsTestExecutionListener 的 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 註解文件章節 以取得範例與更多詳細資訊。