使用内存数据库来为mapper层的接口编写单元测试

简介

使用内存数据库来测试mapper层的sql代码,这种方式可以让测试案例摆脱对数据库的依赖,进而变得可重复执行。

这里选择的内存数据库是h2,它是纯java编写的关系型数据库,开源免费,而且轻量级的,性能较好,可以内嵌进java应用中做内存数据库。

编写方式

开发一个比较基础的组件,必须要为mapper层写单元测试,当前项目之前的sql代码都没有单元测试,同时,每次代码合并时都要跑自动化测试,需要把之前所有的单元测试跑一遍。

在这样的背景下,考虑以内存数据库为基础来为mapper接口编写单元测试,它足够稳定,可以支持自动化测试。

被测代码的sql写在注解上。

实现步骤

基本原理:使用内存数据库,构建mabatis的运行环境

第一步:配置建表语句。在项目路径下,编写一个配置文件,里面是每张表的建表语句,要注意,h2数据库的建表语句和其他数据库的略有不同,它不支持索引,因为它的数据是在内存中。

第二步:配置mybatis运行环境

java 复制代码
public class BaseMapperTestConfig {
    // 支持跨线程运行
    private static final ThreadLocal<SqlSession> threadLocalSession = new ThreadLocal<>();

    // 获取单个mapper接口的实例
    public static <T> T getMapper(Class<T> clazz) {
        // 配置MyBatis环境
        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment("dev", transactionFactory, getDataSource());
        Configuration configuration = new Configuration(environment);

        // 添加Mapper扫描路径
        configuration.addMapper(clazz);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        threadLocalSession.set(sqlSession);
        return sqlSession.getMapper(clazz);
    }

    // 获取多个mapper接口的实例,依照传入顺序依次返回
    public static List<Object> getMappers(Class<?> ...clazzs) {
        // 配置MyBatis环境
        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment("dev", transactionFactory, getDataSource());
        Configuration configuration = new Configuration(environment);

        // 添加Mapper扫描路径
        for (Class<?> clazz : clazzs) {
            configuration.addMapper(clazz);
        }
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        threadLocalSession.set(sqlSession);

        // 多个mapper要在同一个sqlSession之下
        List<Object> result = new ArrayList<>();
        for (Class<?> clazz : clazzs) {
            result.add(sqlSession.getMapper(clazz));
        }
        return result;
    }

    // 关闭sqlSession
    public static void close() {
        SqlSession sqlSession = threadLocalSession.get();
        if (sqlSession != null) {
            sqlSession.close();
            threadLocalSession.remove();
        }
    }
    
    // 清空数据表
    public static void clearData(String tableName) throws SQLException {
        SqlSession sqlSession = threadLocalSession.get();
        if (sqlSession != null) {
            Connection connection = sqlSession.getConnection();
            Statement statement = connection.createStatement();
            String sql = "delete from " + tableName;
            // update类型的语句返回false,表示没有resultSet对象
            statement.execute(sql);
            statement.close();
        }
    }

    // 配置数据库连接池,这里就是使用了内存数据库
    private static DataSource getDataSource() {
        EmbeddedDatabaseBuilder databaseBuilder = new EmbeddedDatabaseBuilder();
        return databaseBuilder
                .setType(EmbeddedDatabaseType.H2)
                // 设置数据库名称和锁超时时间,时间是10秒;启用mvcc;设置隔离级别是串行化。
                // 每次都使用不同的数据库实例
                .setName("testdb" + System.currentTimeMillis() +";LOCK_TIMEOUT=10000;MVCC=TRUE;LOCK_MODE=3")
                .addScript("classpath:db/schema.sql") // 启动时初始化建表语句
                .build();
    }
}

第三步:单测案例,从之前配置好的mybatis环境中获取mapper实例,然后测试,每个单测运行前向数据库中插入一条数据,运行后删除数据,确保运行环境的稳定。

java 复制代码
public class MapperTest {
    private static StudentMapper studentMapper;
    private final String TABLE_NAME = "t_student";

    @BeforeClass
    public static void init() {
        studentMapper = BaseMapperTestConfig.getMapper(StudentMapper.class);
    }

    @AfterClass
    public static void destroy() {
        BaseMapperTestConfig.close();
    }

    @Before
    public void before() throws SQLException {
        PO po = createPO(1L, 2L);  // 单测执行前向数据库中插入一条数据
        studentMapper.insert(po);
    }

    @After
    public void after() throws SQLException {
        BaseMapperTestConfig.clearData(TABLE_NAME);  // 单测执行完之后清空数据库
    }

    @Test
    public void testInsert() {
        // 执行insert语句
        PO po = createPO(2L, 3L);
        int insertNum = studentMapper.insert(po);
        assert insertNum == 1;
    }

    public PO createPO(Long d1, Long d2) {
       // 创建一个po类
    }
}

总结:

  • 关键是在单测中配置mybatis的执行环境,这样可以避免启动spring容器,加快测试速度

  • 每个单测执行前,向数据库中插入固定的数据,执行完成后,情况数据库中的数据,保证测试环境的稳定。

  • h2数据库提供了web页面,供用户访问,不过这里并没有用到,建议用户在增删改查四个测试方法中做好充分的断言,保证数据的正确。

踩坑记录

h2数据库 并发修改异常

在使用内存数据库进行单元测试时,一个常见的问题是并发修改异常。这通常发生在多线程环境或多个测试类同时运行时。如果测试类在同一个JVM实例中运行,会共享同一个内存数据库实例,从而导致并发修改问题。

问题原因:

  • 内存数据库共享:在同一个 JVM 中,多个测试类共享同一个内存数据库实例,导致并发操作冲突。
  • 事务管理:测试类之间未正确隔离事务,导致并发操作冲突。
  • 数据库初始化:每个测试类分别进行数据库初始化时,可能会导致并发请求处理不当。

错误案例:获取锁超时,原因是并发修改。具体情况是,单独执行测试类没有问题,通过mvn clean test执行时,某些测试类就会报并发修改异常

text 复制代码
org.apache.ibatis.exceptions.PersistenceException: ### Error updating database.  Cause: org.h2.jdbc.JdbcSQLTimeoutException: Timeout trying to lock table {0}; 
org.h2.message.DbException: Concurrent update in table "SCENE_SET_DEV": another transaction has updated or deleted the same row [90131-199]

解决方案

  1. 使用不同的数据库实例:为每个测试类使用独立的内存数据库实例
java 复制代码
public class TestDataSourceConfig {
    private static DataSource getDataSource() {
        EmbeddedDatabaseBuilder databaseBuilder = new EmbeddedDatabaseBuilder();
        return databaseBuilder
                .setType(EmbeddedDatabaseType.H2)
                // 设置数据库名称,每次都生成一个单独的数据库。
                // 锁超时时间,时间是10秒;
                // 启用mvcc;设置隔离级别是串行化;使用不同的数据库实例
                .setName("testdb" + System.currentTimeMillis() +";LOCK_TIMEOUT=10000;MVCC=TRUE;LOCK_MODE=3")
                .addScript("classpath:db/schema.sql") // 启动时初始化建表语句
                .build();
    }
}

在这段代码中,通过加入 System.currentTimeMillis() 方法确保每次测试都使用唯一的数据库实例。

参考

相关推荐
沸材21 分钟前
Redis——实现消息队列
数据库·redis·消息队列
しかし11811432 分钟前
C语言队列的实现
c语言·开发语言·数据结构·数据库·经验分享·链表
⁤⁢初遇1 小时前
MySQL---数据库基础
数据库
wolf犭良1 小时前
27、Python 数据库操作入门(SQLite)从基础到实战精讲
数据库·python·sqlite
画扇落汗1 小时前
Python 几种将数据插入到数据库的方法(单行插入、批量插入,SQL Server、MySQL,insert into)
数据库·python·sql·mysql
银河系的一束光1 小时前
mysql的下载和安装2025.4.8
数据库·mysql
Full Stack Developme1 小时前
SQL 查询中使用 IN 导致性能问题的解决方法
数据库·sql
神经星星2 小时前
【vLLM 学习】API 客户端
数据库·人工智能·机器学习
小光学长3 小时前
基于flask+vue框架的助贫公益募捐管理系统1i6pi(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库
XiaoLeisj3 小时前
【图书管理系统】深入解析基于 MyBatis 数据持久化操作:全栈开发图书管理系统:查询图书属性接口(注解实现)、修改图书属性接口(XML 实现)
xml·java·数据库·spring boot·sql·java-ee·mybatis