Mybatis单元测试,不使用spring

news2024/11/25 5:40:53

平时开发过程中需要对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都已释放。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mac 四大常用清理软件推荐,软件特色下载教程横向评测

Mac 一般来说基本是不会中毒的&#xff0c;而且像 现在的 windows 也是很少中毒&#xff0c;但我们可能还是需要一款杀毒清理软件&#xff0c;主要是为了清理垃圾&#xff0c;统一查看并管理软件开机自启、权限信息等&#xff0c;统一卸载清理等功能&#xff0c;另外我们可能还…

【机器学习】PyTorch手动实现Logistic回归算法

参考地址&#xff1a;点击打开 计算较为繁琐&#xff0c;需要用到sigmoid函数和梯度下降算法&#xff0c;步骤主要如下&#xff1a; 二项分布概率公式表示最大似然估计和对数化计算求道带入梯度下降算法计算和优化 代码&#xff1a; import numpy as np import matplotlib.py…

05.计算机网络——TCP协议

文章目录 TCP协议段格式TCP交付过程TCP解包过程确认应答机制\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kDvQFCTM-1689855767485)(C:\Users\11794\AppData\Roaming\Typora\typora-user-images\image-20230719204622485.png)\] 32位序号/32位确认…

深度学习anaconda+pycharm+虚拟环境迁移

一、下载好anaconda和pycharm安装包。 下载anaconda:Index of /anaconda/archive/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror pycharm汉化包 二、安装anaconda 深度学习环境配置-Anaconda以及pytorch1.2.0的环境配置&#xff08;Bubbliiiing 深度学习 教程&…

Pycharm远程服务器连接教程

第一步 只有Pycharm专业版才能远程连接服务器 第二步&#xff1a;远程连接部分 点击左上角的号新建一个连接&#xff0c;起一个名字&#xff0c;比如叫dilab191&#xff1a; 设置SSH参数 Tools-Development-Options 第三步, 添加远程服务器解释器部分 File-settings-Project …

spring复习:(50)@Configuration注解配置的singleton的bean是什么时候被创建出来并缓存到容器的?

一、主类&#xff1a; 二、配置类&#xff1a; 三、singleton bean的创建流程 运行到context.refresh(); 进入refresh方法&#xff1a; 向下运行到红线位置时&#xff1a; 会实例化所有的singleton bean.进入finisheBeanFactoryInitialization方法&#xff1a; 向下拖动代…

旧版Xcode文件较大导致下载总是失败但又不能断点续传重新开始的解决方法

问题&#xff1a; 旧版mac下载旧版Xcode时需要进入https://developer.apple.com/download/all/?qxcode下载&#xff0c;但是下载这些文件需要登录。登录后下载中途很容易失败&#xff0c;失败后又必须重新下载。 解决方案&#xff1a; 下载这里面的内容都需要登录&#xff0…

华为、阿里巴巴、字节跳动 100+ Python 面试问题总结(五)

系列文章目录 个人简介&#xff1a;机电专业在读研究生&#xff0c;CSDN内容合伙人&#xff0c;博主个人首页 Python面试专栏&#xff1a;《Python面试》此专栏面向准备面试的2024届毕业生。欢迎阅读&#xff0c;一起进步&#xff01;&#x1f31f;&#x1f31f;&#x1f31f; …

苹果手机IOS自带科学计算器冷门功能使用

前言 事件是这样的&#xff0c;前几天有人想买个斜坡枕&#xff0c;斜坡枕是个直角三角形&#xff0c;已知短直角边长度是14CM&#xff0c;长直角边长度是80CM&#xff0c;他想知道这个斜坡是多少度&#xff0c;我说这个不是很简单吗&#xff1f;计算一下 a r c t a n ( 14 80…

C# List 详解七

目录 42.Sort() 43.ToArray() 44.ToString() 45.TrimExcess() 46.TrueForAll(Predicate) C# List 详解一 1.Add(T)&#xff0c;2.AddRange(IEnumerable)&#xff0c;3.AsReadOnly()&#xff0c;4.BinarySearch(T)&#xff0c; C# List 详解二 5.Cl…

Matlab 刚性问题求解器-ode23s

1、ode23s介绍 ode23s&#xff08;stiff differential equation solver&#xff09;是MATLAB中的一种求解刚性&#xff08;stiff&#xff09;微分方程的数值方法。刚性微分方程通常具有多个时间尺度差异较大的变量&#xff0c;并且其中至少有一个变量具有快速变化的特性。 od…

Antv G6 force分布式布局 icon“+“ “-“收缩自定义,关系图子节点

子节点收缩 const collapseIcon (x, y, r) > {// 折叠return [[M, x - r, y],[a, r, r, 0, 1, 0, r * 2, 0],[a, r, r, 0, 1, 0, -r * 2, 0],[M, x - r 4, y],[L, x - r 2 * r - 4, y]]}const expandIcon (x, y, r) > {// 拓展return [[M, x - r, y],[a, r, r, 0, 1,…

SQL优化——插入数据优化(load指令的使用)

插入数据时的优化主键优化order by优化group by优化limit优化count优化update优化 1.插入数据时的优化 批量插入数据时最好最多别超过一千条&#xff0c;如果一次批量插入几万条数据&#xff0c;可以将其分割成多条insert语句进行插入。 mysql的事务提交方式是默认自动提交的…

Linux 下centos 查看 -std 是否支持 C17

实际工作中&#xff0c;可能会遇到c的一些高级特性&#xff0c;例如std::invoke&#xff0c;此函数是c17才引入的&#xff0c;如何判断当前的gcc是否支持c17呢&#xff0c;这里提供两种办法。 1.根据gcc的版本号来推断 gcc --version&#xff0c;可以查看版本号&#xff0c;笔者…

15.矩阵运算与img2col方式的卷积

使用矩阵计算卷积 GEMM算法 矩阵乘法运算(General Matrix Multiplication)&#xff0c;形如&#xff1a; C A B , A ∈ R m k , B ∈ R k n , C ∈ R m n C AB, A\in \mathbb{R}^{m\times k},B\in \mathbb{R}^{k\times n},C\in \mathbb{R}^{m\times n} CAB,A∈Rmk,B∈Rk…

vite4.x+vue3.x中使用装饰器语法,eslint校验不识别@的报错处理方法

在项目中&#xff0c;使用了pre-commit校验代码&#xff0c;eslint校验无法识别,导致一直无法提交代码&#xff0c;查找了资料&#xff0c;eslint版本过低&#xff0c;不能解决现在遇到的问题 最终正确的配置方法&#xff1a; 装饰器配置文件babel.config.js module.exports …

了解应用层

应用层 1. 概述2. 应用程序组织方式2.1 C/S方式2.1 P2P方式 3. 动态主机配置协议DHCP3.1 DHCP工作流程 4. 域名系统DNS4.1 域名结构4.2 域名分类4.3 域名服务器4.3.1 分类 4.4 DNS域名解析过程 5. 文件传输协议FTP5.1 FTP工作流程 6. 电子邮件系统6.1 邮件信息格式6.2 简单邮件…

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

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

12.面板问题

面板问题 html部分 <h1>Lorem ipsum dolor sit, amet consectetur adipisicing.</h1><div class"container"><div class"faq"><div class"title-box"><h3 class"title">Lorem, ipsum dolor.<…

TypeScript 中的常用类型声明大全

文章目录 基本数据类型1.number类型2.String 类型3. Boolean 类型4. undefined 类型5.Null类型6.Symbol类型7.BigInt类型 引用数据类型8.Array 类型9.Object 类型 TS 新增特性数据类型4.联合类型5.字面量类型6.Any 类型7.unknown 类型8.Void 类型9.never 类型10.对象类型12 tup…