MyBatis单元测试指南:Mock与内存数据库实战
MyBatis单元测试实战:Mock与内存数据库技术详解
作为Java开发中最流行的ORM框架之一,MyBatis的单元测试是保证数据访问层质量的关键环节。本文将深入探讨两种主流的MyBatis测试方案:Mock测试和内存数据库集成,帮助开发者构建可靠的测试体系。
1. 单元测试基础架构
1.1 测试金字塔中的位置
MyBatis的单元测试属于测试金字塔的基础层,具有执行快、反馈及时的特点。理想情况下,数据访问层的测试应该:
- 不依赖真实数据库
- 快速执行(毫秒级)
- 可重复运行
- 独立于其他组件
1.2 测试准备
推荐测试框架组合:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>test</scope>
</dependency>
2. SqlSession的Mock测试
2.1 核心对象Mock
public class UserMapperTest {
@Mock
private SqlSession sqlSession;
@Mock
private UserMapper userMapper;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
when(sqlSession.getMapper(UserMapper.class)).thenReturn(userMapper);
}
}
2.2 常见场景模拟
查询测试:
@Test
void testGetUserById() {
User expectedUser = new User(1L, "testUser");
when(userMapper.getUserById(1L)).thenReturn(expectedUser);
User actualUser = sqlSession.getMapper(UserMapper.class).getUserById(1L);
assertEquals(expectedUser, actualUser);
verify(userMapper, times(1)).getUserById(1L);
}
插入测试:
@Test
void testInsertUser() {
User newUser = new User(null, "newUser");
when(userMapper.insertUser(newUser)).thenReturn(1);
int affectedRows = sqlSession.getMapper(UserMapper.class).insertUser(newUser);
assertEquals(1, affectedRows);
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userMapper).insertUser(userCaptor.capture());
assertEquals("newUser", userCaptor.getValue().getUsername());
}
2.3 动态SQL测试技巧
对于动态SQL,可以验证生成的SQL语句:
@Test
void testDynamicQuery() {
Map<String, Object> params = new HashMap<>();
params.put("name", "test");
params.put("status", 1);
userMapper.dynamicQuery(params);
verify(userMapper).dynamicQuery(argThat(map ->
"test".equals(map.get("name")) &&
1 == map.get("status")
));
}
实践建议:
- 优先Mock接口而非具体实现
- 使用ArgumentCaptor验证复杂参数
- 对事务操作添加回滚测试
- 结合MyBatis-Spring时注意事务管理器的Mock
3. 内存数据库集成测试
3.1 H2数据库配置
test/resources/application-test.properties
:
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MYSQL
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.data-locations=classpath:data.sql
schema.sql
示例:
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
3.2 MyBatis与H2集成
@SpringBootTest
@ActiveProfiles("test")
class UserMapperIntegrationTest {
@Autowired
private UserMapper userMapper;
@Test
void testH2Integration() {
User user = new User(null, "h2User");
userMapper.insert(user);
User found = userMapper.findById(user.getId());
assertEquals("h2User", found.getUsername());
}
}
3.3 高级特性测试
事务测试:
@Test
@Transactional
void testTransactionalOperation() {
User user1 = new User(null, "user1");
User user2 = new User(null, "user2");
userMapper.insert(user1);
userMapper.insert(user2);
assertEquals(2, userMapper.countAll());
// 测试自动回滚
}
批量操作测试:
@Test
void testBatchInsert() {
try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for(int i=0; i<100; i++) {
mapper.insert(new User(null, "batch-" + i));
}
session.commit();
}
assertEquals(100, userMapper.countByPrefix("batch-"));
}
3.4 性能对比
测试方式 | 执行时间(100次) | 内存占用 | 可靠性 |
---|---|---|---|
Mock测试 | 120ms | 低 | 高 |
H2内存数据库 | 450ms | 中 | 高 |
真实数据库 | 3200ms | 高 | 最高 |
实践建议:
- 使用
@DirtiesContext
避免测试间污染 - 对H2不支持的函数使用替换方案
- 重要路径仍需真实数据库集成测试
- 结合Flyway管理测试数据库版本
4. 测试策略选择
4.1 决策树
4.2 混合测试策略
- 单元测试层:使用Mock验证业务逻辑
- 集成测试层:H2验证SQL正确性
- 验收测试层:真实数据库或TestContainers
示例组合:
class UserServiceTest {
// 快速反馈测试
@Test
void testBusinessLogicWithMock() {
// Mock测试核心逻辑
}
// 全面验证测试
@Test
@Sql(scripts = "/init-test-data.sql")
void testFullIntegrationWithH2() {
// 内存数据库测试
}
}
5. 常见问题解决方案
问题1:H2与生产数据库语法差异
解决方案:
@Configuration
public class H2Config {
@Bean
@Profile("test")
public DatabaseIdProvider databaseIdProvider() {
return new VendorDatabaseIdProvider() {{
setProperties(new Properties() {{
put("H2", "mysql");
// 伪装成MySQL
}});
}};
}
}
问题2:MyBatis缓存干扰测试
解决方案:
@SpringBootTest
@Transactional
@AutoConfigureCache(cacheNames = "none")
class NoCacheTest {
// 测试代码
}
问题3:大量重复测试数据
使用Builder模式简化对象创建:
public class UserTestBuilder {
public static User.UserBuilder basic() {
return User.builder()
.username("default")
.status(1)
.createdAt(LocalDateTime.now());
}
}
// 测试中使用
User user = UserTestBuilder.basic()
.username("custom").build();
结语
有效的MyBatis测试需要根据场景灵活选择策略。对于日常开发,建议:
- 80% Mock测试保证快速反馈
- 15% H2集成测试验证SQL
- 5% 真实环境端到端测试
通过合理的测试分层,可以在保证质量的同时最大化开发效率。记住:没有完美的测试方案,只有最适合当前项目阶段的方案。