当针对关系型数据库编写集成测试时,运行 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 运行这些脚本:

  1. @Test
  2. void databaseTest() {
  3. ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
  4. populator.addScripts(
  5. new ClassPathResource("test-schema.sql"),
  6. new ClassPathResource("test-data.sql"));
  7. populator.setSeparator("@@");
  8. populator.execute(this.dataSource);
  9. // 运行使用测试模式和数据的代码
  10. }

注意 ResourceDatabasePopulator 内部委托给 ScriptUtils 来解析和运行 SQL 脚本。同样,AbstractTransactionalJUnit4SpringContextTests AbstractTransactionalTestNGSpringContextTests 中的 executeSqlScript(..)方法在内部使用 ResourceDatabasePopulator 来运行 SQL 脚本。更多细节请参见各种 executeSqlScript(.)方法的 Javadoc。

用 @Sql 声明式地执行 SQL 脚本

除了上述以编程方式运行 SQL 脚本的机制外,你还可以在 Spring TestContext 框架中声明性地配置 SQL 脚本。具体来说,你可以在测试类或测试方法上声明 @Sql注解,以配置单个 SQL 语句或 SQL 脚本的资源路径,这些脚本应该在集成测试方法之前或之后针对给定的数据库运行。对 @Sql的支持是由 SqlScriptsTestExecutionListener 提供的,它在默认情况下是启用的。

:::info 方法级的 @Sql 声明默认会覆盖类级的声明。然而,从 Spring Framework 5.2 开始,这种行为可以通过 @SqlMergeMode 对每个测试类或每个测试方法进行配置。更多细节请参见使用 @SqlMergeMode 进行合并和覆盖配置。 :::

路径资源语义

每个路径都被解释为一个 Spring Resource。一个普通的路径(例如,schema.sql)被视为一个相对于测试类所定义的包的 classpath 资源。以斜线开头的路径被视为绝对的 classpath 资源(例如,"/org/example/schema.sql")。引用 URL 的路径(例如,以 classpath:file:http:为前缀的路径)通过使用指定的资源协议加载。

下面的例子显示了如何在一个基于 JUnit Jupiter 的集成测试类中,在类级和方法级使用 @Sql。

  1. @SpringJUnitConfig
  2. @Sql("/test-schema.sql")
  3. class DatabaseTests {
  4. @Test
  5. void emptySchemaTest() {
  6. // run code that uses the test schema without any test data
  7. }
  8. @Test
  9. @Sql({"/test-schema.sql", "/test-user-data.sql"})
  10. void userTest() {
  11. // run code that uses the test schema and test data
  12. }
  13. }

默认脚本检测

如果没有指定 SQL 脚本或语句,将尝试检测一个默认脚本,这取决于 @Sql的声明位置。如果不能检测到默认脚本,就会抛出一个 IllegalStateException。

  • 类级声明:如果注解的测试类是 com.example.MyTest,相应的默认脚本是 classpath:com/example/MyTest.sql
  • 方法级声明:如果被注解的测试方法被命名为 testMethod()并且定义在 com.example.MyTest类中,那么相应的默认脚本是classpath:com/example/MyTest.testMethod.sql

声明多个 @Sql 集

如果你需要为一个给定的测试类或测试方法配置多套 SQL 脚本,但每套脚本有不同的语法配置、不同的错误处理规则或不同的执行阶段,你可以声明 @Sql 的多个实例。在 Java 8 中,你可以将 @Sql 作为一个可重复的注释。否则,你可以使用 @SqlGroup 注解作为声明多个 @Sql 实例的明确容器。

下面的例子展示了如何使用 @Sql 作为 Java 8 的可重复注解:

  1. @Test
  2. @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
  3. @Sql("/test-user-data.sql")
  4. void userTest() {
  5. // run code that uses the test schema and test data
  6. }

在前面的例子中介绍的情况下,test-schema.sql 脚本对单行注解使用了不同的语法。

下面的例子与前面的例子相同,只是 @Sql 的声明被分组在 @SqlGroup 中。在 Java 8 及以上版本中,@SqlGroup 的使用是可选的,但你可能需要使用 @SqlGroup 来与其他 JVM 语言(如 Kotlin)兼容:

  1. @Test
  2. @SqlGroup({
  3. @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
  4. @Sql("/test-user-data.sql")
  5. )}
  6. void userTest() {
  7. // run code that uses the test schema and test data
  8. }

脚本执行阶段

默认情况下,SQL 脚本会在相应的测试方法之前运行。然而,如果你需要在测试方法之后运行一组特定的脚本(例如,清理数据库状态),你可以使用 @Sql 中的 executionPhase 属性,如下例所示。

  1. @Test
  2. @Sql(
  3. scripts = "create-test-data.sql",
  4. config = @SqlConfig(transactionMode = ISOLATED)
  5. )
  6. @Sql(
  7. scripts = "delete-test-data.sql",
  8. config = @SqlConfig(transactionMode = ISOLATED),
  9. executionPhase = AFTER_TEST_METHOD
  10. )
  11. void userTest() {
  12. // 运行需要测试数据提交的代码
  13. // 到数据库中,而不是在测试的事务中。
  14. }

使用 @SqlConfig 的脚本配置

你可以通过使用 @SqlConfig 注解来配置脚本解析和错误处理。当作为一个集成测试类的类级注解声明时,@SqlConfig 作为测试类层次结构中所有 SQL 脚本的全局配置。当通过使用 @Sql 注解的 config 属性直接声明时,@SqlConfig 作为本地配置,用于在包围的 @Sql 注解中声明的 SQL 脚本。@SqlConfig 中的每个属性都有一个隐含的默认值,它被记录在相应属性的 javadoc 中。由于《Java 语言规范》中为注解属性定义的规则,不幸的是,不可能为注解属性分配一个空值。因此,为了支持对继承的全局配置的覆盖,@SqlConfig 属性有一个明确的默认值,即 ""(对于字符串)、{}(对于数组)或者 DEFAULT(对于枚举)。这种方法让 @SqlConfig 的本地声明通过提供""{}DEFAULT以外的值来选择性地覆盖 @SqlConfig 的全局声明中的个别属性。只要本地的 @SqlConfig 属性没有提供""{}DEFAULT以外的明确值,全局的 @SqlConfig属性就会被继承。因此,明确的本地配置会覆盖全局配置。

@Sql 和 @SqlConfig 提供的配置选项与 ScriptUtils 和 ResourceDatabasePopulator 支持的配置选项相当,但它们是 <jdbc:initialize-database/> XML 命名空间元素提供的选项的超集。详情请见 @Sql @SqlConfig 中各个属性的 javadoc。

用于 @Sql 的事务管理

默认情况下,SqlScriptsTestExecutionListener 为通过使用 @Sql 配置的脚本推断出所需的事务语义。具体来说,SQL 脚本在没有事务的情况下运行,在现有的 Spring 管理的事务中运行(例如,由 TransactionalTestExecutionListener 管理的带有 @Transactional 注解的测试的事务),或者在一个孤立的事务中运行,这取决于 @SqlConfig 中 transactionMode 属性的配置值以及测试的 ApplicationContext 中是否存在 PlatformTransactionManager。然而,作为最低限度,在测试的 ApplicationContext 中必须有一个 javax.sql.DataSource

如果 SqlScriptsTestExecutionListener 用来检测 DataSource 和 PlatformTransactionManager 并推断交易语义的算法不适合你的需要,你可以通过设置 @SqlConfig 的 dataSource 和 transactionManager 属性来指定明确的名字。此外,你可以通过设置 @SqlConfig的transactionMode 属性来控制事务传播行为(例如,脚本是否应该在一个隔离的事务中运行)。尽管彻底讨论所有支持的 @Sql 事务管理选项超出了本参考手册的范围,但 @SqlConfig SqlScriptsTestExecutionListener 的 javadoc 提供了详细的信息,下面的例子显示了一个典型的测试场景,该场景使用 JUnit Jupiter 和 @Sql 的事务测试。

  1. @SpringJUnitConfig(TestDatabaseConfig.class)
  2. @Transactional
  3. class TransactionalSqlScriptsTests {
  4. final JdbcTemplate jdbcTemplate;
  5. @Autowired
  6. TransactionalSqlScriptsTests(DataSource dataSource) {
  7. this.jdbcTemplate = new JdbcTemplate(dataSource);
  8. }
  9. @Test
  10. @Sql("/test-data.sql")
  11. void usersTest() {
  12. // 验证测试数据库中的状态。
  13. assertNumUsers(2);
  14. // run code that uses the test data...
  15. }
  16. int countRowsInTable(String tableName) {
  17. return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
  18. }
  19. void assertNumUsers(int expected) {
  20. assertEquals(expected, countRowsInTable("user"),
  21. "Number of rows in the [user] table.");
  22. }
  23. }

注意,在运行 usersTest()方法后,不需要清理数据库,因为对数据库所做的任何改变(无论是在测试方法中还是在 /test-data.sql脚本中)都会被 TransactionalTestExecutionListener 自动回滚(详见事务管理)。

用 @SqlMergeMode 合并和重写配置

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