为什么要使用H2做单元测试
1. 内存模式运行(零配置、零残留)
- 无需安装:只需添加依赖即可使用,不依赖外部服务
- 内存数据库:通过
jdbc:h2:mem:testdb
配置,数据完全存在于内存中 - 测试后自动销毁:进程结束即消失,不会残留测试数据污染环境
- 示例:
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
2. 极速启动与执行
- 毫秒级启动:比 MySQL/PostgreSQL 等传统数据库快 10-100 倍
- 高性能:内存操作避免磁盘 I/O 瓶颈,加速测试执行
- 统计对比:
数据库 启动时间 简单查询延迟 H2 50ms 0.1ms MySQL 2s+ 1-10ms
3. 隔离性与可重复性
- 独立实例:每个测试用例可创建独立数据库实例
- 事务支持:配合
@Transactional
实现自动回滚 - 代码示例:
@Test @Transactional // 测试后自动回滚 public void testInsert() { repository.save(new Entity()); assertEquals(1, repository.count()); // 断言生效 } // 此处自动回滚,不影响其他测试
4. 兼容性强大
- 多模式支持:可模拟其他数据库行为
jdbc:h2:mem:test;MODE=MySQL # 模拟MySQL语法
- 兼容常见SQL:支持标准 SQL、存储过程、触发器等功能
- 规避问题:测试时暴露 SQL 兼容性问题,避免生产环境踩坑
5. 开发体验优化
- 嵌入式控制台:通过 Web 界面实时查看测试数据
spring.h2.console.enabled=true spring.h2.console.path=/h2-console
- 自动初始化:配合
schema.sql
/data.sql
快速构建测试场景 - 与框架深度集成:
- Spring Boot 自动配置
- MyBatis/JPA 无缝衔接
- Testcontainers 兼容
6. CI/CD 友好
- 无外部依赖:适合 Docker/CI 环境(如 GitHub Actions)
- 资源占用低:仅需 2MB JAR 文件,不消耗额外内存
- 并行测试:支持多线程同时运行测试类
对比传统数据库的劣势
场景 | H2 表现 | 生产数据库表现 |
---|---|---|
复杂查询优化 | 无查询优化器 | 有优化器 |
大数据量测试 | 内存限制(约 1GB 数据) | 支持 TB 级数据 |
数据库特性测试 | 部分语法不兼容 | 完全兼容 |
何时不适合用 H2?
- 需要测试数据库专属特性(如 Oracle 的窗口函数)
- 需要验证超大数据量性能
- 测试特定数据库的兼容性(此时建议用 Testcontainers + 真实数据库)
最佳实践建议
- 基础测试:95% 的 CRUD/业务逻辑测试用 H2
- 集成测试:结合
@Testcontainers
补充真实数据库测试 - 配置示例:
# src/test/resources/application-test.yml spring: datasource: url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1 username: sa password: ""
H2 通过极简的设计实现了单元测试的核心需求——快速、隔离、可重复,这正是它成为 Java 单元测试首选数据库的原因。
具体实践
首先看我的项目结构,非常的简单,而且大部分都是使用 mybatis-plus 的插件自动生成的
这里要说明几点:
- test/resources 中的 application配置文件如果存在的话,那么运行单元测试的时候是会首先加载这个配置文件的,而且这个配置文件在正常启动项目的时候,也不会被加载,所以,放心大胆的使用
- schema.sql 和 data.sql 文件:首先我们要知道 h2 数据库是内存数据库,也就是说,保留不下来表结构这些东西,不像mysql一样会将数据放到磁盘里面,所以每次我们使用 h2 的时候,都要重新的 建个表,加载一些初始数据
test下的配置文件如下
spring:
datasource:
url: spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL;IFEXISTS=TRUE
driver-class-name: org.h2.Driver
username: sa
password: ""
# 初始化数据库脚本
schema: classpath:schema.sql
data: classpath:data.sql
initialization-mode: embedded
h2:
console:
enabled: true
path: /h2-console
# MyBatis-Plus 配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 输出SQL到控制台
global-config:
db-config:
id-type: auto # 主键策略
mapper-locations: classpath*:mapper/**/*.xml
pom 文件如下,pom文件不区分 测试和正式
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- h2 数据库-->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version> <!-- 请根据需要选择合适的版本 -->
</dependency>
单元测试的书写
@SpringBootTest
@Transactional // 测试后自动回滚
public class StudentMapperTest {
@Autowired
private StudentMapper studentMapper;
@Test
public void testSelectAll() {
List<Student> students = studentMapper.selectList(null);
Assertions.assertEquals(3, students.size()); // 验证data.sql中的数据
}
@Test
public void testInsert() {
Student newStudent = new Student();
newStudent.setName("赵六");
int result = studentMapper.insert(newStudent);
Assertions.assertEquals(1, result);
Student dbStudent = studentMapper.selectById(newStudent.getId());
Assertions.assertEquals("赵六", dbStudent.getName());
}
}
以上代码经过实地的验证,可以运行