网站界面设计需要首先做市场研究湘潭建设网站公司
简介
使用内存数据库来测试mapper层的sql代码,这种方式可以让测试案例摆脱对数据库的依赖,进而变得可重复执行。
这里选择的内存数据库是h2,它是纯java编写的关系型数据库,开源免费,而且轻量级的,性能较好,可以内嵌进java应用中做内存数据库。
编写方式
开发一个比较基础的组件,必须要为mapper层写单元测试,当前项目之前的sql代码都没有单元测试,同时,每次代码合并时都要跑自动化测试,需要把之前所有的单元测试跑一遍。
在这样的背景下,考虑以内存数据库为基础来为mapper接口编写单元测试,它足够稳定,可以支持自动化测试。
被测代码的sql写在注解上。
实现步骤
基本原理:使用内存数据库,构建mabatis的运行环境
第一步:配置建表语句。在项目路径下,编写一个配置文件,里面是每张表的建表语句,要注意,h2数据库的建表语句和其他数据库的略有不同,它不支持索引,因为它的数据是在内存中。
第二步:配置mybatis运行环境
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;}// 关闭sqlSessionpublic 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实例,然后测试,每个单测运行前向数据库中插入一条数据,运行后删除数据,确保运行环境的稳定。
public class MapperTest {private static StudentMapper studentMapper;private final String TABLE_NAME = "t_student";@BeforeClasspublic static void init() {studentMapper = BaseMapperTestConfig.getMapper(StudentMapper.class);}@AfterClasspublic static void destroy() {BaseMapperTestConfig.close();}@Beforepublic void before() throws SQLException {PO po = createPO(1L, 2L); // 单测执行前向数据库中插入一条数据studentMapper.insert(po);}@Afterpublic void after() throws SQLException {BaseMapperTestConfig.clearData(TABLE_NAME); // 单测执行完之后清空数据库}@Testpublic 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
执行时,某些测试类就会报并发修改异常
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]
解决方案
- 使用不同的数据库实例:为每个测试类使用独立的内存数据库实例
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() 方法确保每次测试都使用唯一的数据库实例。
参考
- https://www.jianshu.com/p/3f34b1c584c3