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