执行 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 脚本。同样,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 脚本或语句,则会尝试根据 @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 注解文档部分

© . This site is unofficial and not affiliated with VMware.