平时开发过程中需要对mybatis的Mapper类做单元测试,主要是验证语法是否正确,尤其是一些复杂的动态sql,一般项目都集成了spring或springboot,当项比较大时,每次单元测试启动相当慢,可能需要好几分钟,因此写了一个纯mybatis的单元测试基类,实现单元测试的秒级启动。
单元测试基类
MybatisBaseTest
类主要完成如下工作:
1.加载mybatis配置文件
在MybatisBaseTest.init()方法实现,
该动作在整个单元测试生命周期只执行一次,并且在启动前执行 ,
因此使用junit的@BeforeClass注解标注,表示该动作在单元测试启动前执行。
2.打开session
在MybatisBaseTest.openSession()方法实现,
该方法获取一个mybatis的SqlSession,并将SqlSession存入到线程本地变量中,
使用junit的@Before注解标注,表示在每一个单元测试方法运行前都执行该动作。
3.mapper对象注入
单元测试子类中通过在字段上使用@javax.annotation.Resource注解自动注入Mapper对象,子类测试方法中可以直接使用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
类的代码如下:
packagecom.zhouyong.practice.mybatis.base;importorg.apache.ibatis.builder.xml.XMLConfigBuilder;importorg.apache.ibatis.builder.xml.XMLMapperBuilder;importorg.apache.ibatis.io.Resources;importorg.apache.ibatis.session.Configuration;importorg.apache.ibatis.session.SqlSession;importorg.apache.ibatis.session.SqlSessionFactory;importorg.apache.ibatis.session.SqlSessionFactoryBuilder;importorg.junit.After;importorg.junit.Before;importorg.junit.BeforeClass;importorg.springframework.context.support.FileSystemXmlApplicationContext;importorg.springframework.core.io.Resource;importorg.springframework.util.StringUtils;importjava.io.IOException;importjava.io.InputStream;importjava.lang.reflect.Field;importjava.sql.Connection;importjava.sql.SQLException;importjava.util.ArrayList;importjava.util.List;importjava.util.Properties;/**
* mybatis单元测试基类
* @author zhouyong
* @date 2023/7/23 9:45 上午
*/publicclassMybatisBaseTest{privatefinalstaticString configLocation ="mybatis/mybatis-config-test.xml";privatestaticThreadLocal<LocalSession> sessionThreadLocal;privatestaticList<LocalSession> sessionPool;privatestaticSqlSessionFactory sqlSessionFactory;/**
* 单元类测试启动前的初始化动作
* 初始化数据库session等相关信息
*/@BeforeClasspublicfinalstaticvoidinit()throwsIOException{//多个单元测试类批量执行时,init方法会重复执行,因此做空判断避免重复执行if(sqlSessionFactory!=null){return;}//解析mybatis全局配置文件Configuration configuration =parseConfiguration();//解析mapper配置parseMapperXmlResource(configuration);//创建SqlSessionFactory
sqlSessionFactory =newSqlSessionFactoryBuilder().build(configuration);//用于存储所有的session
sessionPool =newArrayList<>();//LocalSession的线程本地变量
sessionThreadLocal =newThreadLocal<>();//保底操作,确保异常退出时关闭所有数据库连接Runtime.getRuntime().addShutdownHook(newThread(()->closeAllSession()));}/**
* 启动session并且注入mapper对象
* 每一个单元测试方法启动之前会自动执行该方法
* 如果子类也有@Before方法,父类的@Before方法先于子类执行
*/@BeforepublicfinalvoidopenSessionAndInjectMapper(){LocalSession localSession =createLocalSession();
sessionThreadLocal.set(localSession);
sessionPool.add(localSession);injectMapper();}/**
* mapper代理对象注入
*/privatevoidinjectMapper(){Class<?extendsMybatisBaseTest> testClass =this.getClass();Field[] fields = testClass.getDeclaredFields();for(Field field : fields){if(field.getAnnotation(javax.annotation.Resource.class)!=null){boolean accessible = field.isAccessible();try{if(!accessible){
field.setAccessible(true);}Object mapperObj = sessionThreadLocal.get().getMapper(field.getType());
field.set(this,mapperObj);}catch(IllegalAccessException e){thrownewRuntimeException("mapper对象注入失败:"+field.getName(),e);}finally{
field.setAccessible(accessible);}}}}/**
* 关闭session
* 每一个单元测试执行完之后都会自动执行该方法
* 如果子类也有@After方法,则子类的@After方法先于父类执行(于@Before方法相反)
*/@AfterpublicfinalvoidcloseSession(){LocalSession localSession = sessionThreadLocal.get();if(localSession!=null){
localSession.close();
sessionPool.remove(localSession);
sessionThreadLocal.remove();}}/**
* 保底操作,异常退出时关闭所有session
*/publicfinalstaticvoidcloseAllSession(){if(sessionPool!=null){for(LocalSession localSession : sessionPool){
localSession.close();}
sessionPool.clear();
sessionPool =null;}
sessionThreadLocal =null;}/**
* 解析mybatis全局配置文件
* @throws IOException
*/privatefinalstaticConfigurationparseConfiguration()throwsIOException{InputStream inputStream =Resources.getResourceAsStream(configLocation);XMLConfigBuilder parser =newXMLConfigBuilder(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
*/privatefinalstaticvoidparseMapperXmlResource(Configuration configuration)throwsIOException{String[] mapperLocations = configuration.getVariables().getProperty("mapperLocations").split(",");//借助spring的FileSystemXmlApplicationContext工具类,根据配置匹配解析出所有路径FileSystemXmlApplicationContext xmlContext =newFileSystemXmlApplicationContext();for(String mapperLocation : mapperLocations){Resource[] mapperResources = xmlContext.getResources(mapperLocation);for(Resource mapperRes : mapperResources){XMLMapperBuilder xmlMapperBuilder =newXMLMapperBuilder(mapperRes.getInputStream(),
configuration,
mapperRes.toString(),
configuration.getSqlFragments());
xmlMapperBuilder.parse();}}}/**
* 创建自定义的LocalSession
* @return
*/privatefinalLocalSessioncreateLocalSession(){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);returnnewLocalSession(sqlSession, connection, isCommit);}catch(SQLException e){thrownewRuntimeException(e);}}}
LocalSession
类对SqlSession做了一层封装
packagecom.zhouyong.practice.mybatis.base;importorg.apache.ibatis.session.SqlSession;importjava.sql.Connection;importjava.sql.SQLException;/**
* @author zhouyong
* @date 2023/7/23 9:52 上午
*/publicclassLocalSession{/** mybatis 的 session */privateSqlSession session;/** sql 的 connection */privateConnection connection;/** 是否提交事物,单元测试一般不需要提交事物(直接回滚) */privateboolean isCommit;publicLocalSession(SqlSession session,Connection connection,boolean isCommit)throwsSQLException{this.isCommit = isCommit;this.session = session;this.connection = connection;}/**
* 获取mapper对象
* @param mapperClass
* @param <T>
* @return
*/public<T>TgetMapper(Class<T> mapperClass){return session.getMapper(mapperClass);}/**
* 关闭session
* @throws SQLException
*/publicvoidclose(){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" ?><!DOCTYPEconfigurationPUBLIC"-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><propertiesresource="mybatis/mybatis-db-test.properties"></properties><settings><!-- 打印查询语句 --><settingname="logImpl"value="STDOUT_LOGGING"/><!-- 控制全局缓存(二级缓存)--><settingname="cacheEnabled"value="false"/><!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载,增加启动效率。默认 false --><settingname="lazyLoadingEnabled"value="true"/><!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖--><settingname="aggressiveLazyLoading"value="false"/></settings><environmentsdefault="development"><environmentid="development"><transactionManagertype="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 --><dataSourcetype="POOLED"><propertyname="driver"value="${jdbc.driver}"/><propertyname="url"value="${jdbc.url}"/><propertyname="username"value="${jdbc.username}"/><propertyname="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
:
packagecom.zhouyong.practice.mybatis.test;importcom.zhouyong.practice.mybatis.base.MybatisBaseTest;importorg.junit.Test;importjavax.annotation.Resource;importjava.util.List;/**
*
* @author zhouyong
* @date 2023/7/23 12:32 下午
*/publicclassCustomerMapperTestextendsMybatisBaseTest{/**
* 支持自动注入Mapper对象
*/@ResourceprivateCustomerMapper customerMapper;@Testpublicvoidtest1(){List<CustomerEntity> list = customerMapper.selectAll();System.out.println("1 list.size()=="+list.size());CustomerEntity entity =newCustomerEntity();
entity.setName("李四");
entity.setAge(55);
entity.setSex("男");
customerMapper.insertMetrics(entity);
list = customerMapper.selectAll();System.out.println("2 list.size()=="+list.size());}@Testpublicvoidtest2(){List<CustomerEntity> metricsEntities = customerMapper.selectAll();System.out.println("3 list.size()=="+metricsEntities.size());CustomerEntity entity =newCustomerEntity();
entity.setName("王五");
entity.setAge(55);
entity.setSex("男");
customerMapper.insertMetrics(entity);
metricsEntities = customerMapper.selectAll();System.out.println("4 list.size()=="+metricsEntities.size());}}
测试结果符合预期,运行完成后没有提交事务(因为配置中的isCommit设置为false),且单元测试运行完之后所有的connection都已释放。
版权归原作者 薛定谔的雄猫 所有, 如有侵权,请联系我们删除。