执行 SQL 脚本
在针对关系数据库编写集成测试时,运行 SQL 脚本来修改数据库架构或将测试数据插入表中通常是有益的。
infra-jdbc 模块提供了通过在加载 Infra ApplicationContext 时执行 SQL 脚本来 初始化 嵌入式或现有数据库的支持。
有关详细信息,请参阅 嵌入式数据库支持 和 使用嵌入式数据库测试数据访问逻辑。
虽然在加载 ApplicationContext 时初始化数据库 一次 非常有用,但有时必须能够在集成测试 期间 修改数据库。
以下部分说明了如何在集成测试期间以编程方式和声明方式运行 SQL 脚本。
以编程方式执行 SQL 脚本
Infra 提供了以下选项,用于在集成测试方法中以编程方式执行 SQL 脚本。
-
infra.jdbc.datasource.init.ScriptUtils -
infra.jdbc.datasource.init.ResourceDatabasePopulator -
infra.test.context.junit4.AbstractTransactionalJUnit4InfraContextTests -
infra.test.context.testng.AbstractTransactionalTestNGInfraContextTests
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
@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);
// 运行使用测试架构和数据的代码
}
请注意,ResourceDatabasePopulator 在内部委托给 ScriptUtils 来解析和运行 SQL 脚本。
同样,AbstractTransactionalJUnit4InfraContextTests 和 AbstractTransactionalTestNGInfraContextTests 中的 executeSqlScript(..) 方法在内部使用 ResourceDatabasePopulator 来运行 SQL 脚本。
有关更多详细信息,请参阅各种 executeSqlScript(..) 方法的 Javadoc。
使用 @Sql 以声明方式执行 SQL 脚本
除了上述以编程方式运行 SQL 脚本的机制外,您还可以在 Infra TestContext 框架中以声明方式配置 SQL 脚本。
具体来说,您可以在测试类或测试方法上声明 @Sql 注解,以配置应在集成测试类或测试方法之前或之后针对给定数据库运行的单个 SQL 语句或 SQL 脚本的资源路径。
@Sql 的支持由 SqlScriptsTestExecutionListener 提供,默认情况下已启用。
|
默认情况下,方法级 但是,这不适用于为 |
路径资源语义
每个路径都解释为 Infra Resource。
普通路径(例如,"schema.sql")被视为相对于定义测试类的包的类路径资源。
以斜杠开头的路径被视为绝对类路径资源(例如,"/org/example/schema.sql")。
引用 URL 的路径(例如,以 classpath:、file:、http: 为前缀的路径)使用指定的资源协议加载。
以下示例显示了如何在基于 JUnit Jupiter 的集成测试类中在类级别和方法级别使用 @Sql:
-
Java
@JUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
@Test
void emptySchemaTest() {
// 运行使用测试架构但不包含任何测试数据的代码
}
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})
void userTest() {
// 运行使用测试架构和测试数据的代码
}
}
默认脚本检测
如果没有指定 SQL 脚本或语句,则会尝试根据声明 @Sql 的位置检测 default 脚本。
如果无法检测到默认值,则抛出 IllegalStateException。
-
类级声明:如果带注解的测试类是
com.example.MyTest,则对应的默认脚本是classpath:com/example/MyTest.sql。 -
方法级声明:如果带注解的测试方法名为
testMethod()并且定义在类com.example.MyTest中,则对应的默认脚本是classpath:com/example/MyTest.testMethod.sql。
记录 SQL 脚本和语句
如果您想查看正在执行哪些 SQL 脚本,请将 infra.test.context.jdbc 日志类别设置为 DEBUG。
如果您想查看正在执行哪些 SQL 语句,请将 infra.jdbc.datasource.init 日志类别设置为 DEBUG。
声明多个 @Sql 集
如果您需要为给定的测试类或测试方法配置多组 SQL 脚本,但具有不同的语法配置、不同的错误处理规则或每组不同的执行阶段,则可以声明多个 @Sql 实例。
您可以将 @Sql 用作可重复注解,也可以使用 @SqlGroup 注解作为声明多个 @Sql 实例的显式容器。
以下示例显示了如何将 @Sql 用作可重复注解:
-
Java
@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
// 运行使用测试架构和测试数据的代码
}
在前面的示例中呈现的场景中,test-schema.sql 脚本对单行注释使用不同的语法。
以下示例与前面的示例相同,只是 @Sql 声明在 @SqlGroup 中组合在一起。
使用 @SqlGroup 是可选的,但您可能需要使用 @SqlGroup 才能与其他 JVM 语言兼容。
-
Java
@Test
@SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
)}
void userTest() {
// 运行使用测试架构和测试数据的代码
}
脚本执行阶段
默认情况下,SQL 脚本在相应的测试方法之前运行。
但是,如果您需要在测试方法之后运行一组特定的脚本(例如,为了清理数据库状态),您可以将 @Sql 中的 executionPhase 属性设置为 AFTER_TEST_METHOD,如下例所示:
-
Java
@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() {
// 运行需要将测试数据提交到数据库的代码
// 并且该代码在测试事务之外运行
}
ISOLATED 和 AFTER_TEST_METHOD 分别从 Sql.TransactionMode 和 Sql.ExecutionPhase 静态导入。
|
从 TODAY Framework 6.1 开始,可以通过将类级 @Sql 声明中的 executionPhase 属性设置为 BEFORE_TEST_CLASS 或 AFTER_TEST_CLASS 来在测试类之前或之后运行一组特定的脚本,如下例所示:
-
Java
@JUnitConfig
@Sql(scripts = "/test-schema.sql", executionPhase = BEFORE_TEST_CLASS)
class DatabaseTests {
@Test
void emptySchemaTest() {
// 运行使用测试架构但不包含任何测试数据的代码
}
@Test
@Sql("/test-user-data.sql")
void userTest() {
// 运行使用测试架构和测试数据的代码
}
}
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 提供的配置选项等同于 ScriptUtils 和 ResourceDatabasePopulator 支持的选项,但它们是 <jdbc:initialize-database/> XML 命名空间元素提供的选项的超集。
有关详细信息,请参阅 @Sql 和 @SqlConfig 中各个属性的 javadoc。
@Sql 的事务管理
默认情况下,SqlScriptsTestExecutionListener 推断使用 @Sql 配置的脚本所需的事务语义。
具体来说,SQL 脚本将在没有事务的情况下运行、在现有的 Infra 管理的事务中运行(例如,由 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 的事务性测试的典型测试场景:
-
Java
@JUnitConfig(TestDatabaseConfig.class)
@Transactional
class TransactionalSqlScriptsTests {
final JdbcTemplate jdbcTemplate;
@Autowired
TransactionalSqlScriptsTests(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Test
@Sql("/test-data.sql")
void usersTest() {
// 验证测试数据库中的状态:
assertNumUsers(2);
// 运行使用测试数据的代码...
}
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.");
}
}
请注意,运行 usersTest() 方法后无需清理数据库,因为对数据库所做的任何更改(无论是在测试方法内还是在 /test-data.sql 脚本内)都会由 TransactionalTestExecutionListener 自动回滚(有关详细信息,请参阅 事务管理)。
使用 @SqlMergeMode 合并和覆盖配置
自 TODAY Framework 5.2 起,可以将方法级 @Sql 声明与类级声明合并。
例如,这允许您为每个测试类提供一次数据库架构或一些通用测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。
要启用 @Sql 合并,请使用 @SqlMergeMode(MERGE) 注解您的测试类或测试方法。
要禁用特定测试方法(或特定测试子类)的合并,您可以通过 @SqlMergeMode(OVERRIDE) 切换回默认模式。
有关示例和更多详细信息,请参阅 @SqlMergeMode 注解文档部分。