平时开发过程中需要对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等相关信息*/@BeforeClasspublic final static void init() throws SQLException, IOException {//解析mybatis全局配置文件Configuration configuration = parseConfiguration();//解析mapper配置parseMapperXmlResource(configuration);//创建SqlSessionFactorysqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);//用于存储所有的sessionsessionPool = new ArrayList<>();//LocalSession的线程本地变量sessionThreadLocal = new ThreadLocal<>();//保底操作,确保异常退出时关闭所有数据库连接Runtime.getRuntime().addShutdownHook(new Thread(()->closeAllSession()));}/*** 启动session* 每一个单元测试方法启动之前会自动执行该方法* 如果子类也有@Before方法,父类的@Before方法先于子类执行*/@Beforepublic 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方法相反)*/@Afterpublic 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 {@Testpublic 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());}@Testpublic 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都已释放。