执行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);
	// 运行使用测试模式和数据的代码
}
@Test
fun databaseTest() {
	val populator = ResourceDatabasePopulator()
	populator.addScripts(
			ClassPathResource("test-schema.sql"),
			ClassPathResource("test-data.sql"))
	populator.setSeparator("@@")
	populator.execute(dataSource)
	// 运行使用测试模式和数据的代码
}
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:为前缀的路径)将使用指定的资源协议加载。

以下示例展示了如何在基于JUnit Jupiter的集成测试类中在类级别和方法级别使用@Sql

  • Java

  • Kotlin

@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {

	@Test
	void emptySchemaTest() {
		// 运行使用测试模式但没有测试数据的代码
	}

	@Test
	@Sql({"/test-schema.sql", "/test-user-data.sql"})
	void userTest() {
		// 运行使用测试模式和测试数据的代码
	}
}
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {

	@Test
	fun emptySchemaTest() {
		// 运行使用测试模式但没有测试数据的代码
	}

	@Test
	@Sql("/test-schema.sql", "/test-user-data.sql")
	fun userTest() {
		// 运行使用测试模式和测试数据的代码
	}
}

默认脚本检测

如果未指定任何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() {
	// 运行使用测试模式和测试数据的代码
}
@Test
@Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
fun userTest() {
	// 运行使用测试模式和测试数据的代码
}

在前面示例中,test-schema.sql脚本使用了不同的单行注释语法。

以下示例与前面的示例相同,只是@Sql声明在@SqlGroup中分组。使用@SqlGroup是可选的,但您可能需要使用@SqlGroup以与其他JVM语言兼容。

  • Java

  • Kotlin

@Test
@SqlGroup({
	@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
	@Sql("/test-user-data.sql")
)}
void userTest() {
	// 运行使用测试模式和测试数据的代码
}
@Test
@SqlGroup(
	Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
	Sql("/test-user-data.sql")
)
fun userTest() {
	// 运行使用测试模式和测试数据的代码
}

脚本执行阶段

默认情况下,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() {
	// 在测试的事务之外运行需要测试数据的代码
}
@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() {
	// 在测试的事务之外运行需要测试数据的代码
}
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() {
		// 在没有任何测试数据的情况下使用测试模式的代码
	}

	@Test
	@Sql("/test-user-data.sql")
	void userTest() {
		// 使用测试模式和测试数据的代码
	}
}
@SpringJUnitConfig
@Sql("/test-schema.sql", executionPhase = BEFORE_TEST_CLASS)
class DatabaseTests {

	@Test
	fun emptySchemaTest() {
		// 在没有任何测试数据的情况下使用测试模式的代码
	}

	@Test
	@Sql("/test-user-data.sql")
	fun userTest() {
		// 使用测试模式和测试数据的代码
	}
}
BEFORE_TEST_CLASSSql.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进行配置合并和覆盖

从Spring Framework 5.2开始,可以将方法级别的@Sql声明与类级别的声明合并。例如,这使您可以为每个测试类一次性提供数据库模式或一些常见测试数据的配置,然后为每个测试方法提供额外的、特定用例的测试数据。要启用@Sql合并,可以在测试类或测试方法中注释@SqlMergeMode(MERGE)。要禁用特定测试方法(或特定测试子类)的合并,可以通过@SqlMergeMode(OVERRIDE)切换回默认模式。请参阅注解文档部分中的@SqlMergeMode注解的示例和更多详细信息。