简介
使用内存数据库来测试mapper层的sql代码,这种方式可以让测试案例摆脱对数据库的依赖,进而变得可重复执行。
这里选择的内存数据库是h2,它是纯java编写的关系型数据库,开源免费,而且轻量级的,性能较好,可以内嵌进java应用中做内存数据库。
编写方式
开发一个比较基础的组件,必须要为mapper层写单元测试,当前项目之前的sql代码都没有单元测试,同时,每次代码合并时都要跑自动化测试,需要把之前所有的单元测试跑一遍。
在这样的背景下,考虑以内存数据库为基础来为mapper接口编写单元测试,它足够稳定,可以支持自动化测试。
被测代码的sql写在注解上。
实现步骤
基本原理:使用内存数据库,构建mabatis的运行环境
第一步:配置建表语句。在项目路径下,编写一个配置文件,里面是每张表的建表语句,要注意,h2数据库的建表语句和其他数据库的略有不同,它不支持索引,因为它的数据是在内存中。
第二步:配置mybatis运行环境
java
public class BaseMapperTestConfig {
// 支持跨线程运行
private static final ThreadLocal<SqlSession> threadLocalSession = new ThreadLocal<>();
// 获取单个mapper接口的实例
public static <T> T getMapper(Class<T> clazz) {
// 配置MyBatis环境
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("dev", transactionFactory, getDataSource());
Configuration configuration = new Configuration(environment);
// 添加Mapper扫描路径
configuration.addMapper(clazz);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSession sqlSession = sqlSessionFactory.openSession();
threadLocalSession.set(sqlSession);
return sqlSession.getMapper(clazz);
}
// 获取多个mapper接口的实例,依照传入顺序依次返回
public static List<Object> getMappers(Class<?> ...clazzs) {
// 配置MyBatis环境
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("dev", transactionFactory, getDataSource());
Configuration configuration = new Configuration(environment);
// 添加Mapper扫描路径
for (Class<?> clazz : clazzs) {
configuration.addMapper(clazz);
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSession sqlSession = sqlSessionFactory.openSession();
threadLocalSession.set(sqlSession);
// 多个mapper要在同一个sqlSession之下
List<Object> result = new ArrayList<>();
for (Class<?> clazz : clazzs) {
result.add(sqlSession.getMapper(clazz));
}
return result;
}
// 关闭sqlSession
public static void close() {
SqlSession sqlSession = threadLocalSession.get();
if (sqlSession != null) {
sqlSession.close();
threadLocalSession.remove();
}
}
// 清空数据表
public static void clearData(String tableName) throws SQLException {
SqlSession sqlSession = threadLocalSession.get();
if (sqlSession != null) {
Connection connection = sqlSession.getConnection();
Statement statement = connection.createStatement();
String sql = "delete from " + tableName;
// update类型的语句返回false,表示没有resultSet对象
statement.execute(sql);
statement.close();
}
}
// 配置数据库连接池,这里就是使用了内存数据库
private static DataSource getDataSource() {
EmbeddedDatabaseBuilder databaseBuilder = new EmbeddedDatabaseBuilder();
return databaseBuilder
.setType(EmbeddedDatabaseType.H2)
// 设置数据库名称和锁超时时间,时间是10秒;启用mvcc;设置隔离级别是串行化。
// 每次都使用不同的数据库实例
.setName("testdb" + System.currentTimeMillis() +";LOCK_TIMEOUT=10000;MVCC=TRUE;LOCK_MODE=3")
.addScript("classpath:db/schema.sql") // 启动时初始化建表语句
.build();
}
}
第三步:单测案例,从之前配置好的mybatis环境中获取mapper实例,然后测试,每个单测运行前向数据库中插入一条数据,运行后删除数据,确保运行环境的稳定。
java
public class MapperTest {
private static StudentMapper studentMapper;
private final String TABLE_NAME = "t_student";
@BeforeClass
public static void init() {
studentMapper = BaseMapperTestConfig.getMapper(StudentMapper.class);
}
@AfterClass
public static void destroy() {
BaseMapperTestConfig.close();
}
@Before
public void before() throws SQLException {
PO po = createPO(1L, 2L); // 单测执行前向数据库中插入一条数据
studentMapper.insert(po);
}
@After
public void after() throws SQLException {
BaseMapperTestConfig.clearData(TABLE_NAME); // 单测执行完之后清空数据库
}
@Test
public void testInsert() {
// 执行insert语句
PO po = createPO(2L, 3L);
int insertNum = studentMapper.insert(po);
assert insertNum == 1;
}
public PO createPO(Long d1, Long d2) {
// 创建一个po类
}
}
总结:
-
关键是在单测中配置mybatis的执行环境,这样可以避免启动spring容器,加快测试速度
-
每个单测执行前,向数据库中插入固定的数据,执行完成后,情况数据库中的数据,保证测试环境的稳定。
-
h2数据库提供了web页面,供用户访问,不过这里并没有用到,建议用户在增删改查四个测试方法中做好充分的断言,保证数据的正确。
踩坑记录
h2数据库 并发修改异常
在使用内存数据库进行单元测试时,一个常见的问题是并发修改异常。这通常发生在多线程环境或多个测试类同时运行时。如果测试类在同一个JVM实例中运行,会共享同一个内存数据库实例,从而导致并发修改问题。
问题原因:
- 内存数据库共享:在同一个 JVM 中,多个测试类共享同一个内存数据库实例,导致并发操作冲突。
- 事务管理:测试类之间未正确隔离事务,导致并发操作冲突。
- 数据库初始化:每个测试类分别进行数据库初始化时,可能会导致并发请求处理不当。
错误案例:获取锁超时,原因是并发修改。具体情况是,单独执行测试类没有问题,通过mvn clean test
执行时,某些测试类就会报并发修改异常
text
org.apache.ibatis.exceptions.PersistenceException: ### Error updating database. Cause: org.h2.jdbc.JdbcSQLTimeoutException: Timeout trying to lock table {0};
org.h2.message.DbException: Concurrent update in table "SCENE_SET_DEV": another transaction has updated or deleted the same row [90131-199]
解决方案
- 使用不同的数据库实例:为每个测试类使用独立的内存数据库实例
java
public class TestDataSourceConfig {
private static DataSource getDataSource() {
EmbeddedDatabaseBuilder databaseBuilder = new EmbeddedDatabaseBuilder();
return databaseBuilder
.setType(EmbeddedDatabaseType.H2)
// 设置数据库名称,每次都生成一个单独的数据库。
// 锁超时时间,时间是10秒;
// 启用mvcc;设置隔离级别是串行化;使用不同的数据库实例
.setName("testdb" + System.currentTimeMillis() +";LOCK_TIMEOUT=10000;MVCC=TRUE;LOCK_MODE=3")
.addScript("classpath:db/schema.sql") // 启动时初始化建表语句
.build();
}
}
在这段代码中,通过加入 System.currentTimeMillis() 方法确保每次测试都使用唯一的数据库实例。