每次执行@Test方法前都执行一次DB初始化(SpringBoot Test + JUnit5环境)

本文介绍了在Spring应用中如何使用H2内存数据库替代线上MySQL数据库进行单元测试,讨论了使用@Sql注解和ResourceDatabasePopulator在每次单元测试前初始化数据库的方法,以确保数据一致性并减少依赖。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

在执行单元测试时,可以使用诸如H2内存数据库替代线上的Mysql数据库等,如此在执行单元测试时就能尽可能模拟真实环境的SQL执行,同时也无需依赖线上数据库,增加了测试用例执行环境的可移植性。而使用H2数据库时,通常会在执行单元测试前先初始化数据库,即执行SQL脚本来对H2内存数据库进行初始化。
例如可通过如下配置指定H2的初始化脚本:

注: 如下配置中的
spring.sql.init.schema-locations
spring.sql.init.data-locations
即对应数据库schema(table定义)、data(数据插入)的导入脚本。

spring:
  # Sql初始化配置
  sql:
    init:
      # 导入h2 table定义
      schema-locations: classpath:h2/demo-schema.sql
      # 导入h2 数据定义
      data-locations: classpath:h2/demo-data.sql
  # 数据库配置
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    # ============================================================
    # ============= 使用H2内存数据库 ================================
    # ============================================================
    driver-class-name: org.h2.Driver
    # 使用h2内存数据(以mysql兼容模式运行)
    url: jdbc:h2:mem:rbac;MODE=MySQL;DATABASE_TO_LOWER=TRUE
    username: root
    password: 123456

相关SQL脚本示例如下:

-- h2/demo-schema.sql定义
-- 注意create table前都先执行drop table脚本,如此便可保证Sql脚本可以被覆盖执行
DROP TABLE IF EXISTS category;
CREATE TABLE category (
	id bigint(20) NOT NULL COMMENT '主键ID',
	parent_category_id bigint(20) DEFAULT NULL COMMENT '上级分类ID',
	category_name varchar(64) NOT NULL COMMENT '分类名称',
	category_desc varchar(512) DEFAULT NULL COMMENT '分类描述',
	create_time datetime NOT NULL COMMENT '创建时间',
	update_time datetime DEFAULT NULL COMMENT '修改时间'
) ENGINE = InnoDB;


-- ------------------------------------------------------------------------
-- h2/demo-data.sql定义
-- 注意先执行schema定义sql,再执行data定义sql,多个sql脚本需考虑好先后执行顺序
INSERT INTO category
(id,   parent_category_id, category_name, category_desc,      create_time,                update_time) VALUES
( 1,   null,               '分类1',       '查询测试专用数据',    '2023-01-01T15:58:04.105',   '2023-01-07T15:58:04.105'),
( 11,   1,                 '分类1-1',     '分类1-1描述',        '2023-01-02T15:58:04.105',   '2023-01-07T15:58:04.105');

但问题是如上SQL初始化配置会在Spring环境启动时仅执行一次,后续的单元测试都是作用在这同一套脚本的基础之上,不同的单元测试都对这套基础数据进行修改之后,可能会导致数据混乱,进而导致单元测试执行失败。

比较理想的状态就是在每个单元测试方法执行之前,都执行一遍数据库初始化,如此便能保证每个单元测试方法执行前的数据库数据都是固定的,不受其他单元测试的影响,如此在编写单元测试时都以统一的基础数据为基准,无需考虑前后单元测试间的数据依赖,减轻了单元测试方法的开发难度。

接下来本文主要介绍两种在每次单元测试方法执行前都会通过SQL脚本对数据库进行始化的方式。

方式1: @Sql

可在单元测试类 或者 单元测试方法上使用@Sql注解,

import org.springframework.test.context.jdbc.Sql;

@Sql(
        //SQL初始化脚本(会按照数组声明顺序依次执行Sql脚本)
        scripts = {
                "classpath:h2/schema.sql",
                "classpath:h2/data.sql"
        },
        //SQL初始化执行阶段(BEFORE_TEST_METHOD: 测试方法执行前, AFTER_TEST_METHOD: 测试方法执行后)
        executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD
)
@SpringBootTest
public class MyBizTest {
	
	//具体方法上也可使用@Sql注解,此处@Sql定义会覆盖类上的@Sql
	//可通过在测试方法 或 测试类上 使用@SqlMergeMode(MergeMode.OVERRIDE|MERGE)来设置方法的@Sql是覆盖、还是合并类上的@Sql
	@Test
	void testMyBiz() {
		//......
	}
}

如果定义了基础测试类,也可将@Sql直接定义在基础测试类上:

import org.springframework.test.context.jdbc.Sql;

@Sql(
        //SQL初始化脚本(会按照数组声明顺序依次执行Sql脚本)
        scripts = {
                "classpath:rbac-h2/schema.sql",
                "classpath:rbac-h2/data.sql"
        },
        //SQL初始化执行阶段(BEFORE_TEST_METHOD: 方法执行前, AFTER_TEST_METHOD: 方法执行后)
        executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD
)
@SpringBootTest
public class BaseTest {
	//......
}

---

public class MyBizTest extends BaseTest {
	
	@Test
	void testMyBiz() {
		//......
	}
}

对@Sql的支持是由org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener提供的,并且它是默认启用的。
在这里插入图片描述
关于@Sql、@SqlConfig、@SqlMergeMode、@SqlGroup的更多使用可参见:
https://docs.spring.io/spring-framework/docs/5.3.29/reference/html/testing.html#testcontext-executing-sql-declaratively

方式2:ResourceDatabasePopulator

除了通过注解的方式,也可以通过编程的方式执行数据库初始脚本。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。

ResourceDatabasePopulator在内部委托ScriptUtils解析和运行SQL脚本。类似地,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests中的executeSqlScript(…)方法在内部使用ResourceDatabasePopulator来运行SQL脚本。

如下使用ResourceDatabasePopulator ,结合@BeforeEach在每个@Test方法执行前对数据库进行初始化:

@SpringBootTest
public class MyBizTest {

	//注入数据源
	@Resource
    private DataSource dataSource;
    
    
    //@BeforeEach即对应没个@Test方法执行前,
    //亦可通过@BeforeAll指定在每个测试类执行前执行(需在测试类上标注@TestInstance(TestInstance.Lifecycle.PER_CLASS))
    @BeforeEach 
    public void beforeEachTestMethod() {
        //声明ResourceDatabasePopulator 
        ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
        //加载SQL脚本,此处直接使用ClassPathResource, 所以具体path无需以classpath:开头
        //会按照数组声明顺序依次执行Sql脚本
        ClassPathResource[] scriptResources = Stream.of(
                "h2/schema.sql",
                "h2/data.sql"
        ).map(ClassPathResource::new).toArray(ClassPathResource[]::new);
        //添加SQL脚本
        populator.addScripts(scriptResources);
        //使用数据源执行SQL脚本
        populator.execute(this.dataSource);
    }

	@Test
	void testMyBiz() {
		//......
	}    
}

如果定义了基础测试类,也可将@BeforeEach逻辑直接定义在基础测试类上:

@SpringBootTest
public class BaseTest {

	//注入数据源
	@Resource
    private DataSource dataSource;    
    
    //@BeforeEach即对应没个@Test方法执行前,
    //亦可通过@BeforeAll指定在每个测试类执行前执行(需在测试类上标注@TestInstance(TestInstance.Lifecycle.PER_CLASS))
    @BeforeEach 
    public void beforeEachTestMethod() {
        //声明ResourceDatabasePopulator 
        ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
        //加载SQL脚本,此处直接使用ClassPathResource, 所以具体path无需以classpath:开头
        //会按照数组声明顺序依次执行Sql脚本
        ClassPathResource[] scriptResources = Stream.of(
                "h2/schema.sql",
                "h2/data.sql"
        ).map(ClassPathResource::new).toArray(ClassPathResource[]::new);
        //添加SQL脚本
        populator.addScripts(scriptResources);
        //使用数据源执行SQL脚本
        populator.execute(this.dataSource);
    }
}

---

public class MyBizTest extends BaseTest {
	
	@Test
	void testMyBiz() {
		//......
	}
}

参考:
https://docs.spring.io/spring-framework/docs/5.3.29/reference/html/testing.html#testcontext-executing-sql

### 关于 `@SpringBootTest` 注解的使用 在测试环境中,`@SpringBootTest` 是用于加载完整的应用程序上下文并执行集成测试的关键注解。此注解允许开发者针对整个应用或者特定切面进行全面的功能验证[^1]。 对于苍穹外卖这样的复杂项目而言,在编写单元测试或集成测试时可以利用 `@SpringBootTest` 来启动真实的 Spring 应用程序环境。下面是一个简单的示例展示如何在一个测试类上运用这个注解: ```java import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class OrderServiceTest { @Autowired private OrderService orderService; @Test public void testPlaceOrder() { // 测试逻辑... } } ``` 上述代码片段展示了怎样通过 `@SpringBootTest` 启动整个 Spring 上下文,并自动装配所需的组件实例以便进行功能性的测试操作[^2]。 值得注意的是,当采用此类式进行测试时,默认情况下会尝试读取 application.properties 或者 application.yml 文件中的配置项来初始化应用程序上下文。如果希望指定不同的配置文件路径,则可以通过设置参数的式调整行为模式,例如: ```java @SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT) ``` 这里设置了随机端口以供 HTTP 请求访问,这对于涉及网络通信的服务层接口尤其有用[^3]。 #### 配置数据源或其他外部资源 有时为了更好地模拟真实场景下的数据库交互等情况,可以在测试期间引入 H2 数据库或者其他内存型的数据存储解决案作为替代案之一;也可以借助 MockMvc 对控制器层面的行为展开更细致入微地考察。 ```yaml spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MYSQL driverClassName: org.h2.Driver username: sa password: jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop ``` 以上 YAML 片段提供了一种可能的式来定义一个临时性的嵌入式数据库连接字符串以及 JPA/Hibernate 的相关选项,从而使得测试过程更加贴近实际生产环境的要求[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗小爬EX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值