Mybatis单元测试,不使用spring

平时开发过程中需要对mybatis的Mapper类做单元测试,主要是验证语法是否正确,尤其是一些复杂的动态sql,一般项目都集成了spring或springboot,当项比较大时,每次单元测试启动相当慢,可能需要好几分钟,因此写了一个纯mybatis的单元测试基类,实现单元测试的秒级启动。

单元测试基类MybatisBaseTest类主要完成如下工作:

1.加载mybatis配置文件

在MybatisBaseTest.init()方法实现,

该动作在整个单元测试生命周期只执行一次,并且在启动前执行 ,

因此使用junit的@BeforeClass注解标注,表示该动作在单元测试启动前执行。
2.打开session

在MybatisBaseTest.openSession()方法实现,

该方法获取一个mybatis的SqlSession,并将SqlSession存入到线程本地变量中,

使用junit的@Before注解标注,表示在每一个单元测试方法运行前都执行该动作。
3.获取mapper对象

在MybatisBaseTest提供getMapper(Class mapperClass)方法供单元测试子类使用,用于获取具体的Mapper代理对象做测试。
4.关闭session

在MybatisBaseTest.closeSession()方法实现,

从线程本地变量中获取SqlSession对象,完成事务的回滚(单元测试一般不提交事务)和connection的关闭等逻辑。

使用junit的@After注解标注,表示该动作在每一个单元测试方法运行完成后执行。
源码地址: mybatis测试基类

整体包结构如下:

需要的Maven依赖如下

xml 复制代码
<!-- mybatis依赖 -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.5</version>
</dependency>
<!-- 单元测试junit包 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
</dependency>
<!-- 用到spring的FileSystemXmlApplicationContext工具类来加载配置 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

MybatisBasetTest类的代码如下:

java 复制代码
package com.zhouyong.practice.mybatis.base;

import org.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * mybatis单元测试基类
 * @author zhouyong
 * @date 2023/7/23 9:45 上午
 */
public class MybatisBaseTest {

    private static ThreadLocal<LocalSession> sessionThreadLocal;

    private static SqlSessionFactory sqlSessionFactory;

    //配置文件的路径  
    private final static String configLocation = "mybatis/mybatis-config-test.xml";

    private static List<LocalSession> sessionPool;

    /**
     * 单元测试启动前的初始化动作
     * 初始化数据库session等相关信息
     */
    @BeforeClass
    public final static void init() throws SQLException, IOException {
        //解析mybatis全局配置文件
        Configuration configuration = parseConfiguration();
        //解析mapper配置
        parseMapperXmlResource(configuration);
        //创建SqlSessionFactory
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

        //用于存储所有的session
        sessionPool = new ArrayList<>();
        //LocalSession的线程本地变量
        sessionThreadLocal = new ThreadLocal<>();
        //保底操作,确保异常退出时关闭所有数据库连接
        Runtime.getRuntime().addShutdownHook(new Thread(()->closeAllSession()));
    }

    /**
     * 启动session
     * 每一个单元测试方法启动之前会自动执行该方法
     * 如果子类也有@Before方法,父类的@Before方法先于子类执行
     */
    @Before
    public final void openSession(){
        LocalSession localSession = createLocalSession();
        sessionThreadLocal.set(localSession);
        sessionPool.add(localSession);
    }

    /**
     * 获取mapper对象
     * @param mapperClass
     * @param <T>
     * @return
     */
    protected final <T> T getMapper(Class<T> mapperClass){
        return sessionThreadLocal.get().getMapper(mapperClass);
    }
    
    /**
     * 关闭session
     * 每一个单元测试执行完之后都会自动执行该方法
     * 如果子类也有@After方法,则子类的@After方法先于父类执行(于@Before方法相反)
     */
    @After
    public final void closeSession(){
        LocalSession localSession = sessionThreadLocal.get();
        if(localSession!=null){
            localSession.close();
            sessionPool.remove(localSession);
            sessionThreadLocal.remove();
        }
    }

    /**
     * 保底操作,异常退出时关闭所有session
     */
    public final static void closeAllSession(){
        if(sessionPool!=null){
            for (LocalSession localSession : sessionPool) {
                localSession.close();
            }
            sessionPool.clear();
            sessionPool = null;
        }
        sessionThreadLocal = null;
    }

    /**
     * 解析mybatis全局配置文件
     * @throws IOException
     */
    private final static Configuration parseConfiguration() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream(configLocation);
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream);
        Configuration configuration = parser.parse();

        //驼峰命名自动转换
        configuration.setMapUnderscoreToCamelCase(true);

        Properties properties = configuration.getVariables();
        //如果密码有加密,则此处可以进行解密
        //String pwd = properties.getProperty("jdbcPassword");
        //((PooledDataSource)configuration.getEnvironment().getDataSource()).setPassword("解密后的密码");

        return configuration;
    }

    /**
     * 解析mapper配置文件
     * @throws IOException
     */
    private final static void parseMapperXmlResource(Configuration configuration) throws IOException {
        String[] mapperLocations = configuration.getVariables().getProperty("mapperLocations").split(",");
        //借助spring的FileSystemXmlApplicationContext工具类,根据配置匹配解析出所有路径
        FileSystemXmlApplicationContext xmlContext = new FileSystemXmlApplicationContext();

        for (String mapperLocation : mapperLocations) {
            Resource[] mapperResources = xmlContext.getResources(mapperLocation);
            for (Resource mapperRes : mapperResources) {
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperRes.getInputStream(),
                        configuration,
                        mapperRes.toString(),
                        configuration.getSqlFragments());
                xmlMapperBuilder.parse();
            }

        }
    }

    /**
     * 创建自定义的LocalSession
     * @return
     */
    private final LocalSession createLocalSession(){
        try{
            String isCommitStr = sqlSessionFactory.getConfiguration().getVariables().getProperty("isCommit");
            boolean isCommit = StringUtils.isEmpty(isCommitStr) ? false : Boolean.parseBoolean(isCommitStr);

            SqlSession sqlSession = sqlSessionFactory.openSession(false);
            Connection connection = sqlSession.getConnection();
            connection.setAutoCommit(false);

            return new LocalSession(sqlSession, connection, isCommit);
        }catch (SQLException e){
            throw new RuntimeException(e);
        }
    }

}

LocalSession类对SqlSession做了一层封装

java 复制代码
package com.zhouyong.practice.mybatis.base;

import org.apache.ibatis.session.SqlSession;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * @author zhouyong
 * @date 2023/7/23 9:52 上午
 */
public class LocalSession {

    /** mybatis 的 session */
    private SqlSession session;

    /** sql 的 connection */
    private Connection connection;

    /** 是否提交事物,单元测试一般不需要提交事物(直接回滚) */
    private boolean isCommit;

    public LocalSession(SqlSession session, Connection connection, boolean isCommit) throws SQLException {
        this.isCommit = isCommit;
        this.session = session;
        this.connection = connection;
    }

    /**
     * 获取mapper对象
     * @param mapperClass
     * @param <T>
     * @return
     */
    public <T> T getMapper(Class<T> mapperClass){
        return session.getMapper(mapperClass);
    }

    /**
     * 关闭session
     * @throws SQLException
     */
    public void close(){
        try{
            if(isCommit){
                connection.commit();
            }else{
                connection.rollback();
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            try{
                session.close();
            }catch (Exception e) {
                e.printStackTrace();
            }/*finally {
                try {
                    if(!connection.isClosed()){
                        connection.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }*/
        }
    }

}

mybatis-config-test.xml配置文件

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>

    <properties resource="mybatis/mybatis-db-test.properties"></properties>

    <settings>
        <!-- 打印查询语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
        <!-- 控制全局缓存(二级缓存)-->
        <setting name="cacheEnabled" value="false"/>
        <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载,增加启动效率。默认 false  -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

</configuration>

mybatis-db-test.properties配置文件

bash 复制代码
#扫描mapper.xml的路径,多个用英文逗号隔开
mapperLocations=classpath:mapper/*.xml

#是否提交事务,单元测试一般不提交设置为false即可
isCommit=false

#数据库连接参数配置
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mysql?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
jdbc.username=root
jdbc.password=123456

测试类CustomerMapperTest继承MybatisBaseTest

java 复制代码
package com.zhouyong.practice.mybatis;

import com.zhouyong.practice.mybatis.base.MybatisBaseTest;
import com.zhouyong.practice.mybatis.test.CustomerEntity;
import com.zhouyong.practice.mybatis.test.CustomerMapper;
import org.junit.Test;

import java.util.List;

/**
 * 测试类继承MybatisBaseTest类
 * @author zhouyong
 * @date 2023/7/23 12:32 下午
 */
public class CustomerMapperTest extends MybatisBaseTest {

    @Test
    public void test1(){
        CustomerMapper mapper = getMapper(CustomerMapper.class);
        List<CustomerEntity> list = mapper.selectAll();
        System.out.println("1 list.size()=="+list.size());

        CustomerEntity entity = new CustomerEntity();
        entity.setName("李四");
        entity.setAge(55);
        entity.setSex("男");

        mapper.insertMetrics(entity);

        list = mapper.selectAll();
        System.out.println("2 list.size()=="+list.size());
    }

    @Test
    public void test2(){
        CustomerMapper mapper = getMapper(CustomerMapper.class);
        List<CustomerEntity> metricsEntities = mapper.selectAll();
        System.out.println("3 list.size()=="+metricsEntities.size());

        CustomerEntity entity = new CustomerEntity();
        entity.setName("王五");
        entity.setAge(55);
        entity.setSex("男");

        mapper.insertMetrics(entity);

        metricsEntities = mapper.selectAll();
        System.out.println("4 list.size()=="+metricsEntities.size());
    }
}

测试结果符合预期,运行完成后没有提交事务(因为配置中的isCommit设置为false),且单元测试运行完之后所有的connection都已释放。

相关推荐
鹿屿二向箔8 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架的汽车租赁共享平台系统
spring·mvc·mybatis
王解10 小时前
Jest项目实战(4):将工具库顺利迁移到GitHub的完整指南
单元测试·github
沐雪架构师11 小时前
mybatis连接PGSQL中对于json和jsonb的处理
json·mybatis
鹿屿二向箔12 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架的咖啡馆管理系统
spring·mvc·mybatis
Devil枫21 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
aloha_7891 天前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
毕业设计制作和分享1 天前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
paopaokaka_luck1 天前
基于Spring Boot+Vue的助农销售平台(协同过滤算法、限流算法、支付宝沙盒支付、实时聊天、图形化分析)
java·spring boot·小程序·毕业设计·mybatis·1024程序员节
cooldream20091 天前
Spring Boot中集成MyBatis操作数据库详细教程
java·数据库·spring boot·mybatis
小袁在上班1 天前
Python 单元测试中的 Mocking 与 Stubbing:提高测试效率的关键技术
python·单元测试·log4j