MyBatis单元测试实战:Mock与内存数据库技术详解

作为Java开发中最流行的ORM框架之一,MyBatis的单元测试是保证数据访问层质量的关键环节。本文将深入探讨两种主流的MyBatis测试方案:Mock测试和内存数据库集成,帮助开发者构建可靠的测试体系。

1. 单元测试基础架构

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")
    ));
}

实践建议:

  1. 优先Mock接口而非具体实现
  2. 使用ArgumentCaptor验证复杂参数
  3. 对事务操作添加回滚测试
  4. 结合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最高

实践建议:

  1. 使用@DirtiesContext避免测试间污染
  2. 对H2不支持的函数使用替换方案
  3. 重要路径仍需真实数据库集成测试
  4. 结合Flyway管理测试数据库版本

4. 测试策略选择

4.1 决策树

图2

4.2 混合测试策略

  1. 单元测试层:使用Mock验证业务逻辑
  2. 集成测试层:H2验证SQL正确性
  3. 验收测试层:真实数据库或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测试需要根据场景灵活选择策略。对于日常开发,建议:

  1. 80% Mock测试保证快速反馈
  2. 15% H2集成测试验证SQL
  3. 5% 真实环境端到端测试

通过合理的测试分层,可以在保证质量的同时最大化开发效率。记住:没有完美的测试方案,只有最适合当前项目阶段的方案。

添加新评论