如何对springboot mapper 编写单元测试
在 Spring Boot 中对 MyBatis Mapper 编写单元测试的核心目标是隔离真实数据库依赖,验证 SQL 逻辑与数据库交互的正确性。以下是完整的实践指南,涵盖环境配置、测试数据初始化、具体测试用例编写及常见问题解决。
一、环境准备
1. 依赖配置(pom.xml
)
需添加测试相关依赖,确保能启动 Spring 上下文、使用 H2 内存数据库,并集成 MyBatis:
<!-- Spring Boot 测试基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- H2 内存数据库(测试用) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- MyBatis 与 Spring Boot 集成(若项目未引入) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version> <!-- 按项目实际版本调整 -->
</dependency>
<!-- 断言库(可选,推荐 AssertJ) -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
二、核心测试思路
通过 H2 内存数据库模拟真实数据库环境,测试时:
-
启动 Spring 上下文,加载 MyBatis Mapper 和 H2 数据源。
-
初始化 H2 表结构(通过
schema.sql
)和测试数据(通过data.sql
或编程方式)。 -
注入 Mapper 接口,调用数据库操作方法,验证结果是否符合预期。
-
使用
@Transactional
自动回滚测试数据,避免污染后续测试。
三、具体实现步骤
1. 配置测试环境(application-test.properties
)
在 src/test/resources
目录下创建测试专用的配置文件,指定 H2 数据库连接和 MyBatis 配置:
# 数据源配置(H2 内存数据库)
spring.datasource.url=jdbc:h2:mem:user_db;DB_CLOSE_DELAY=-1;MODE=MYSQL;INIT=CREATE SCHEMA IF NOT EXISTS user_db\;
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# H2 控制台(可选,方便调试时查看数据)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# MyBatis 配置(指定 Mapper XML 路径和实体别名包)
mybatis.mapper-locations=classpath:mapper/user/*.xml # 假设 Mapper XML 放在 mapper/user 目录
mybatis.type-aliases-package=com.example.entity.user # 实体类包路径
# 打印 SQL 日志(调试用)
logging.level.com.example.mapper.user=debug
-
MODE=MYSQL
:让 H2 模拟 MySQL 语法(如AUTO_INCREMENT
、LIMIT
),避免因数据库方言差异导致测试失败。 -
INIT=CREATE SCHEMA...
:启动时自动创建数据库模式(可选,也可通过schema.sql
初始化)。
2. 初始化测试数据
测试前需准备数据库表和初始数据,推荐两种方式:
方式 1:SQL 脚本(最常用)
在 src/test/resources
下创建 schema.sql
(表结构)和 data.sql
(测试数据):
-- schema.sql(建表语句,兼容 MySQL)
CREATE TABLE IF NOT EXISTS user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
age INT NOT NULL CHECK (age > 0),
email VARCHAR(100),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- data.sql(插入测试数据)
INSERT INTO user (username, age, email) VALUES ('张三', 20, 'zhangsan@example.com');
INSERT INTO user (username, age, email) VALUES ('李四', 25, 'lisi@example.com');
- 注意:
schema.sql
和data.sql
会在 Spring Boot 启动时自动执行(需确保spring.sql.init.mode=always
,默认已开启)。
方式 2:编程初始化(灵活控制)
通过 @Sql
注解或 CommandLineRunner
在测试启动时动态插入数据:
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
// 测试前插入额外数据(仅执行一次)
@BeforeAll
static void setupTestData(@Autowired DataSource dataSource) throws SQLException {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// 插入一条测试数据(ID 手动指定,避免与 data.sql 冲突)
stmt.executeUpdate("INSERT INTO user (id, username, age, email) VALUES (99, '测试用户', 30, 'test@example.com')");
}
}
}
3. 编写 Mapper 单元测试
以 UserMapper
为例(包含 selectById
、insert
、update
、delete
等方法),测试类需覆盖核心场景。
示例 1:查询操作测试
import com.example.entity.user.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest // 加载 Spring 上下文(自动注入 Mapper)
@Transactional // 测试后自动回滚事务(关键!避免数据污染)
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
// 测试:根据 ID 查询存在的用户
@Test
void selectById_ExistingUser_ReturnsUser() {
// 准备:已知测试数据的 ID(data.sql 插入的第一条数据 ID 为 1)
Integer userId = 1;
// 执行:调用 Mapper 方法
User user = userMapper.selectById(userId);
// 断言:验证返回结果符合预期
assertThat(user)
.isNotNull()
.hasFieldOrPropertyWithValue("username", "张三")
.hasFieldOrPropertyWithValue("age", 20)
.hasFieldOrPropertyWithValue("email", "zhangsan@example.com");
}
// 测试:根据 ID 查询不存在的用户
@Test
void selectById_NonExistingUser_ReturnsNull() {
Integer invalidId = 999; // 不存在的 ID
User user = userMapper.selectById(invalidId);
assertThat(user).isNull();
}
}
示例 2:插入操作测试
@Test
void insert_NewUser_SavesToDatabase() {
// 准备:新用户对象(ID 由数据库自增生成)
User newUser = new User();
newUser.setUsername("王五");
newUser.setAge(28);
newUser.setEmail("wangwu@example.com");
// 执行:调用插入方法
int affectedRows = userMapper.insert(newUser);
// 断言 1:影响行数为 1(插入成功)
assertThat(affectedRows).isEqualTo(1);
// 断言 2:自增 ID 被正确赋值(需数据库支持自增主键)
assertThat(newUser.getId()).isGreaterThan(0);
// 断言 3:查询数据库验证数据已持久化
User savedUser = userMapper.selectById(newUser.getId());
assertThat(savedUser)
.isNotNull()
.hasFieldOrPropertyWithValue("username", "王五")
.hasFieldOrPropertyWithValue("age", 28);
}
示例 3:更新操作测试
@Test
void update_ExistingUser_ModifiesDatabase() {
// 准备:已存在的用户 ID(data.sql 插入的第二条数据 ID 为 2)
Integer userId = 2;
User userToUpdate = new User();
userToUpdate.setId(userId);
userToUpdate.setAge(26); // 修改年龄
userToUpdate.setEmail("lisi_updated@example.com"); // 修改邮箱
// 执行:调用更新方法
int affectedRows = userMapper.updateById(userToUpdate);
// 断言 1:影响行数为 1(更新成功)
assertThat(affectedRows).isEqualTo(1);
// 断言 2:查询数据库验证字段已更新
User updatedUser = userMapper.selectById(userId);
assertThat(updatedUser)
.hasFieldOrPropertyWithValue("age", 26)
.hasFieldOrPropertyWithValue("email", "lisi_updated@example.com");
}
示例 4:删除操作测试
@Test
void delete_ExistingUser_RemovesFromDatabase() {
// 准备:已存在的用户 ID(测试前插入的 ID 为 99)
Integer userIdToDelete = 99;
// 执行:调用删除方法
int affectedRows = userMapper.deleteById(userIdToDelete);
// 断言 1:影响行数为 1(删除成功)
assertThat(affectedRows).isEqualTo(1);
// 断言 2:查询数据库确认用户已删除
User deletedUser = userMapper.selectById(userIdToDelete);
assertThat(deletedUser).isNull();
}
四、关键优化技巧
1. 事务自动回滚(@Transactional
)
-
作用:测试方法执行后自动回滚事务,避免测试数据残留,保证测试独立性。
-
注意 :若测试需要验证事务行为(如
@Transactional
失效场景),可移除该注解,但需手动清理数据。
2. 轻量级测试(不加载 Spring 上下文)
若希望提升测试速度(避免 Spring 上下文启动耗时),可直接通过 MyBatis 的 SqlSessionFactory
手动创建 Mapper 实例:
步骤 1:配置 SqlSessionFactory
(测试专用)
在 src/test/resources
下创建 mybatis-config-test.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="test">
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:user_db;MODE=MYSQL;INIT=CREATE SCHEMA IF NOT EXISTS user_db"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.example.mapper.user"/> <!-- Mapper 接口包路径 -->
</mappers>
</configuration>
步骤 2:编写轻量级测试类
import com.example.entity.user.User;
import com.example.mapper.user.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import static org.assertj.core.api.Assertions.assertThat;
public class UserMapperLightweightTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeAll
static void init() throws IOException {
// 加载 MyBatis 测试配置
String resource = "mybatis-config-test.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
@Test
void selectById_WithLightweight() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 假设已通过 SQL 脚本插入 ID=1 的用户
User user = userMapper.selectById(1);
assertThat(user).isNotNull();
assertThat(user.getUsername()).isEqualTo("张三");
}
}
}
3. 调试技巧
-
查看 H2 控制台 :访问
http://localhost:8080/h2-console
(根据项目端口调整),输入 JDBC URLjdbc:h2:mem:user_db
,可实时查看测试数据。 -
打印 SQL 日志 :在
application-test.properties
中配置logging.level.com.example.mapper.user=debug
,日志会输出实际执行的 SQL 和参数。
五、常见问题解决
1. Mapper 接口无法注入(NoSuchBeanDefinitionException
)
-
原因:Spring 未扫描到 Mapper 接口。
-
解决:
-
在测试类或配置类上添加
@MapperScan("com.example.mapper.user")
(指定 Mapper 接口包路径)。 -
确认 Mapper 接口标注了
@Mapper
注解(或在mybatis-config-test.xml
中通过<mappers>
标签注册)。
-
2. H2 数据库执行 SQL 报错(如语法不兼容)
-
原因 :H2 默认模式与真实数据库(如 MySQL)语法差异(如
AUTO_INCREMENT
vsAUTO_INCREMENT
)。 -
解决:
-
在 H2 连接 URL 中添加
MODE=MYSQL
(或其他数据库模式,如ORACLE
)。 -
检查 SQL 脚本,避免使用特定数据库特有的语法(如 MySQL 的
BACKUP TABLE
)。
-
3. 测试数据未正确初始化(data.sql
未执行)
-
原因 :Spring Boot 未触发
data.sql
执行。 -
解决:
-
确认
data.sql
位于src/test/resources
目录。 -
在
application-test.properties
中添加spring.sql.init.mode=always
(强制初始化)。
-
总结
对 Spring Boot Mapper 编写单元测试的核心步骤:
-
配置 H2 内存数据库:模拟真实数据库环境。
-
初始化测试数据 :通过
schema.sql
和data.sql
准备表结构和初始数据。 -
编写测试用例:覆盖 CRUD 操作,验证结果正确性。
-
事务回滚 :使用
@Transactional
保证测试独立性。 -
轻量级优化 (可选):通过手动创建
SqlSessionFactory
提升测试速度。
通过以上方法,可高效验证 Mapper 层的数据库交互逻辑,确保代码质量。