0%

Spock实践(一)

Spock实践(一)

背景

​ 单元测试算是一个老生常谈的问题了,在实际的情况中,总是需要在单元测试与进度之间进行一个权衡,而往往实际上权衡时都是偏向进度一方,甚至很多自信的开发者都不推崇进行单元测试的编写,觉得单元测试费时费力,并且在需求调整又需要重新进行单元测试的编写,其实还有很大一部分原因是因为目前在Spring体系中,常用的单元测试框架就是基于PowerMock或者MockTo等,由于这些框架依赖Spring容器,所以在编写过程需要频繁的启动Spring容器进行单元测试的测试,导致过程比较繁琐。

​ 最近看到一篇美团的2021博客点击率TOP10中的一篇Spock单元测试框架介绍以及在美团优选的实践,使用全新的Spock框架来进行单元测试,这个框架的好处,无需依赖Spring容器来进行单元测试,并且结合groovy动态语言的特点,提供了一些标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。具体细节参考美团的那篇技术博客。

尝试

对于dao层抽象

​ 在将这篇博客中的一些例子简单跑过之后,Spock确实在单元测试的编写上比较简单且直观,但是由于它不依赖于Spring容器,导致了原来Spring框架为我们封装的一些特性无法使用,比如在我们应用中,到处都是通过tk.mybatis或者mybatis plus的一些通过接口api来动态化SQL,而无需进行手动编写SQL。这些特性实际上是通过Spring容器中在创建特定bean对象时通过spring的扩展点进行处理的。在美团的那篇博客中也提到了如果想测试dao层,可以通过MyBatis的SqlSession启动mapper实例,但是这种方式仅仅获取到的是最基础的ibatis的代理对象,没有了tk或者是mp相关的增强。

​ 由于目前只是想简单的引入Spock到某一个服务中,在实际的业务逻辑中,并不确定引入Spock对单元测试的编写是否会有提高,所以这里仅仅研究了如何在不依赖spring容器的情况下,伪造tk.mybatis查询时的mapper对象。

​ 跟踪Springbean对象的创建过程,发现tk.mybatis主要是靠tk.mybatis.spring.mapper.MapperFactoryBean(注意包路径,ibatis包下有一个同名类)来进行一些增强处理。所以创建一个工具类,专门用来获取对应的mapper代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 通用mapper工具类
*
* @author xiaocainiaoya
* @date 2022/7/30 16:11:26
*/
class MapperUtil {

private static MapperHelper mapperHelper = new MapperHelper()

private static Config config = new Config()

static {
mapperHelper.setConfig(config)
}

static def getMapper(Class clazz){
ClassLoader classLoader = MapperUtil.class.getClassLoader()
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(classLoader.getResourceAsStream("mybatis-config.xml"))

// 模拟tk.mybatis相关创建过程
MapperFactoryBean mapperFactoryBean = new MapperFactoryBean(clazz)
mapperFactoryBean.setSqlSessionFactory(sqlSessionFactory)
mapperFactoryBean.setMapperHelper(mapperHelper)
mapperFactoryBean.afterPropertiesSet()

return mapperFactoryBean.getObject()
}
}

H2内存数据库

​ 在美团的这篇博客中,推荐使用H2数据库进行数据的隔离,在对H2进行一些了解后发现H2mysql的一些语句并不支持,比如建表时的索引之类,所以需要修改原本的建表脚本,在进行一些字段迭代时,可能会比较繁琐。

通过直接使用java.sql.Connection对象来创建数据库连接,并执行建表语句。

1
2
3
4
5
# AUTO_SERVER=TRUE 默认情况下只能单个连接,这个配置用来开启多个连接
jdbc.url=jdbc:h2:~/test;MODE=MySQL;DB_CLOSE_DELAY=-1;IGNORECASE=TRUE;AUTO_SERVER=TRUE
jdbc.driver=org.h2.Driver
jdbc.username=sa
jdbc.password=

执行处理代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void executeSql(String sqlPath) {
// 执行sql文件
File sqlFile = new File(sqlPath)
if(!sqlFile.exists()){
throw new Exception("sql文件不存在")
}
Statement statement = null
try{
statement = CONNECTION.createStatement();
boolean success = statement.execute(sqlFile.text)
// 这里有点不解,无论是脚本执行成功还是执行失败,这里好像返回的都是false
// 目前不影响使用,没有进行深究
success ? println("执行sql文件成功") : println("执行sql文件失败")
}catch(Exception e){
e.printStackTrace()
}finally{
if(statement != null){
statement.close()
}
}
}

DbUnit的使用

​ 根据美团的这篇博客中,使用DbUnit来进行数据层数据的访问控制,也就是在初始化某个单元测试接口时进行的一些准备数据脚本的执行,

基于xml脚本文件

​ 在经过测试发现,DbUnit是通过xml文件来编写插入数据的脚本,这就导致写xml插入数据脚本文件也会花费一些工作量。以下为一个示例,假设要为某个接口准备几十条数据,估计准备数据的过程要疯,后期可以看看DbUnit有没有接口可以通过SQL转换为这个xml文件,或者自己实现一个接口,将SQL转换为xml文件来减少工作流。

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<gpfa_process_business
id="10007"
test_id="1"
name="2"
value = "1"
mark = "测试"
create_time = "2018-01-01"
/>
</dataset>
基于csv脚本文件

DbUnit还支持通过.csv文件进行数据准备,但是比较繁琐的是它好像不能执行具体的文件,需要执行一个目录,并且目录下需要创建一个table-ordering.txt文件,内容为此目录下要执行的csv文件名称,同时csv文件的名称需与表名保持一致。

自定义查询SQL语句

​ 对于在xxxMapper.xml文件中的查询语句,目前不知道因为什么原因,字段值无法赋值到实体上,需要通过as关键字将下划线字段别名为驼峰形式才可在实体中获取到,这个问题无疑是致命的,导致基本上所有的mapper.xml文件都无法进行单元测试。

-------------本文结束感谢您的阅读-------------