執行 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。類似地,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests 中的 executeSqlScript(..) 方法在內部使用 ResourceDatabasePopulator 來執行 SQL 指令碼。詳見各種 executeSqlScript(..) 方法的 Javadoc。

使用 @Sql 宣告式執行 SQL 指令碼

除了上述以程式設計方式執行 SQL 指令碼的機制之外,您還可以在 Spring TestContext 框架中宣告式地配置 SQL 指令碼。具體來說,您可以在測試類或測試方法上宣告 @Sql 註解,以配置在整合測試類或測試方法之前或之後針對給定資料庫執行的單個 SQL 語句或 SQL 指令碼的資源路徑。對 @Sql 的支援由 SqlScriptsTestExecutionListener 提供,該監聽器預設是啟用的。

方法級別的 @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_CLASSSql.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 提供的配置選項與 ScriptUtilsResourceDatabasePopulator 支援的選項等價,但它們是 <jdbc:initialize-database/> XML 名稱空間元素提供的選項的超集。詳見 @Sql@SqlConfig 中各個屬性的 javadoc。

@Sql 的事務管理

預設情況下,SqlScriptsTestExecutionListener 會推斷使用 @Sql 配置的指令碼所需的事務語義。具體來說,SQL 指令碼可以在無事務、現有 Spring 管理的事務內(例如,由為 @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 註解文件部分