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依赖如下

<!-- 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类的代码如下:

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做了一层封装

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 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配置文件

#扫描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

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都已释放。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/43963.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV图像处理-图像分割-MeanShift

MeanShift 1. 基本概念2.代码示例 1. 基本概念 MeanShift严格说来并不是用来对图像进行分割的&#xff0c;而是在色彩层面的平滑滤波。它会中和色彩分布相近的颜色&#xff0c;平滑色彩细节&#xff0c;侵蚀掉面积较小的的颜色区域&#xff0c;它以图像上任意一点P为圆心&…

优思学院|六西格玛案例分析 - 降低焊接缺陷率

大家都知道六西格玛方法中的控制图有助于监测流程的稳定性和识别特有原因的发生。对流程周期性地采样&#xff0c;当测量结果在控制上限和下限内&#xff0c;而且围绕着一条中心线时&#xff0c;我们就说流程是受控的。注意上述控制上限和下限有别于规范限。 我们来看看一家工…

电脑安装双系统ubuntu18.04+windows后开机直接进入Windows解决方法

电脑型号&#xff1a;联想拯救者Y9000K2021H 系统&#xff1a;Windows11Ubuntu18.04双系统 问题&#xff1a;笔记本安装双系统后&#xff0c;Windows系统下处理word或者看论文&#xff1b;Ubuntu18.04系统安装ros进行机械臂控制等的研究。但最近开机后发现没有系统选项了&#…

知识库数据导出为excel-使用JavaScript实现在浏览器中导出Excel文件

我们智能客服知识库机器人已经开发完成&#xff0c;后端数据库是使用的qdrant向量数据库&#xff0c;但是该数据库并没有导出备份功能&#xff0c;所以我按简单的纯前端实现知识库导出excel数据 使用第三方库(如SheetJS) SheetJS是一个流行的JavaScript库&#xff0c;可帮助处理…

App隐私及合规评估服务

随着移动应用种类和数量呈爆发式增长&#xff0c;APP侵害用户权益事件层出不穷&#xff0c;为规范个人信息的收集使用&#xff0c;打击涉及个人信息违法犯罪行为&#xff0c;我国相继出台多个涉及个人信息保护相关法律法规。与此同时&#xff0c;中央网信办、工信部、公安部、市…

获取大疆无人机的飞控记录数据并绘制曲线

机型M350RTK&#xff0c;其飞行记录文件为加密的&#xff0c;我的完善代码如下 gitgithub.com:huashu996/DJFlightRecordParsing2TXT.git 一、下载安装官方的DJIFlightRecord git clone gitgithub.com:dji-sdk/FlightRecordParsingLib.git飞行记录文件在打开【我的电脑】&am…

Istio Pilot源码学习(二):ServiceController服务发现

本文基于Istio 1.18.0版本进行源码学习 4、服务发现&#xff1a;ServiceController ServiceController是服务发现的核心模块&#xff0c;主要功能是监听底层平台的服务注册中心&#xff0c;将平台服务模型转换成Istio服务模型并缓存&#xff1b;同时根据服务的变化&#xff0c…

OpenHarmony与HarmonyOS联系与区别

目录 1. 背景 2.OpenHarmony 3.HarmonyOS 4.鸿蒙生态 5.OpenHarmony与HarmonyOS的技术上实现区别 1.语言支持 2.SDK 的不同 3.运行调测方式不同 4.对APK的兼容性不同 5.包含关系 6.调试命令 6.何时选择OpenHarmony或是HarmonyOS&#xff1f; 1. 背景 开篇就说“关于…

2023最新谷粒商城笔记之Sentinel概述篇(全文总共13万字,超详细)

Sentinel概述 服务流控、熔断和降级 什么是熔断 当扇出链路的某个微服务不可用或者响应时间太长时&#xff0c;会进行服务的降级&#xff0c;**进而熔断该节点微服务的调用&#xff0c;快速返回错误的响应信息。**检测到该节点微服务调用响应正常后恢复调用链路。A服务调用B服…

Spring Security 身份验证的基本类/架构

目录 1、SecurityContextHolder 核心类 2、SecurityContext 接口 3、Authentication 用户认证信息接口 4、GrantedAuthority 拥有权限接口 5、AuthenticationManager 身份认证管理器接口 6、ProviderManager 身份认证管理器的实现 7、AuthenticationProvider 特定类型的…

数字孪生管控系统,智慧园区楼宇合集

智慧园区是指将物联网、大数据、人工智能等技术应用于传统建筑和基础设施&#xff0c;以实现对园区的全面监控、管理和服务的一种建筑形态。通过将园区内设备、设施和系统联网&#xff0c;实现数据的传输、共享和响应&#xff0c;提高园区的管理效率和运营效益&#xff0c;为居…

【Spring Cloud Gateway 新一代网关】—— 每天一点小知识

&#x1f4a7; S p r i n g C l o u d G a t e w a y 新一代网关 \color{#FF1493}{Spring Cloud Gateway 新一代网关} SpringCloudGateway新一代网关&#x1f4a7; &#x1f337; 仰望天空&#xff0c;妳我亦是行人.✨ &#x1f984; 个人主页——微风撞见云的博客&a…

中医药行业如何进行数字化转型?看天津同仁堂谈“有道有术有零代码”

张伯礼院士曾指出&#xff0c;中药制造的现代化水平&#xff0c;还停留在10%左右的阶段。中医药行业&#xff0c;老字号企业&#xff0c;该如何通过数字化焕发新活力&#xff1f; 天津同仁堂通过与伙伴云合作&#xff0c;零代码构建数字化系统&#xff0c;让技术与思维共同成长…

html,css初学

安装VSCODE ,插件&#xff1a;live server &#xff0c;html support html 然后为了更好地理解&#xff0c;请逐步输入&#xff0c;并及时查看效果 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>D…

PCB封装设计指导(十一)画出脚标,极性标识和特殊器件标识

PCB封装设计指导(十一)画出脚标,极性标识,特殊器件标识 定义完pin number之后,就需要画出器件的脚标,极性标识,特殊标识等丝印相关的信息了,这些说明对辅助PCB布局有很好的作用,当然对后续贴片也很有帮助。 如何添加,具体见如下说明 1.脚标一般都用数字表示,silks…

力扣热门100题之和为k的子数组【中等】

题目描述 给你一个整数数组 nums 和一个整数 k &#xff0c;请你统计并返回 该数组中和为 k 的连续子数组的个数 。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,1], k 2 输出&#xff1a;2 示例 2&#xff1a; 输入&#xff1a;nums [1,2,3], k 3 输出&#xff1a;2 …

Acwing.898 数字三角形(动态规划)

题目 给定一个如下图所示的数字三角形&#xff0c;从顶部出发&#xff0c;在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点&#xff0c;一直走到底层&#xff0c;要求找出─条路径&#xff0c;使路径上的数字的和最大。 输入格式 第一行包含整数n&#xff0…

MES管理系统给汽配企业带来了哪些效益

汽车工业是一个庞大的社会经济系统工程&#xff0c;不同于普通产品&#xff0c;汽车产品是一个高度综合的最终产品&#xff0c;需要组织专业化协作的社会化大生产&#xff0c;需要相关工业产品与之配套。如何提高生产效率和产品质量成为了一个关键问题&#xff0c;而汽配企业ME…

EtherCAT转TCP/IP网关EtherCAT解决方案

你是否曾经为生产管理系统的数据互联互通问题烦恼过&#xff1f;曾经因为协议不同导致通讯问题而感到困惑&#xff1f;现在&#xff0c;我们迎来了突破性的进展&#xff01; 介绍捷米特JM-TCPIP-ECT&#xff0c;一款自主研发的Ethercat从站功能的通讯网关。它能够连接到Etherc…

RDIFramework.NET CS敏捷开发框架 V6.0发布(支持.NET6+、Framework双引擎,全网唯一)

全新RDIFramework.NET V6.0 CS敏捷开发框架发布&#xff0c;全网唯一支持.NET6&#xff0c;Framework双引擎&#xff0c;降低开发成本&#xff0c;提高产品质量&#xff0c;提升用户体验与开发团队稳定性&#xff0c;做软件就选RDIFramework.NET开发框架。 1、RDIFramework.NET…