引言
目前基于java的开发人员的测试框架很多,但基于测试人员的测试框架并不多,我研究了一下,都并不是让我很满意,如使用jmeter,jmeter虽然支持 beanshell,但脚本是 java 语言编写,java 语言本身代码量就比较多,没有python语言简捷,jmeter好处还是有的,jmeter 的界面非常友好,对于不太会写代码的测试人员来说,是不错的选择,jmeter项目及接口管理都比较方便,但我还认为有一些不好的地方,就是 jmeter开发文件需要上传到 git上,同事再从 git 上下载下来,再在本地运行,而这些文件是什么类型,不同的操作系统,不同的 jmeter 版本是否有兼容性问题,不得而知了,但仅从 jmeter 不支持python语法这一点就让我直接放弃使用,让我想到另外一个办法,直接使用 python语言编写测试脚本。但也带来一个问题,接口的管理不如 jmeter 直观。一个后台项目就需建一个python测试项目与之对应,如果后台项目很多,测试项目也会跟着增多,项目管理不方便,如果在测试过程中只想测试部分脚本,python编写的需要修改 python源代码才行,不像 jmeter有启用禁用按钮,因此急需一个既像 jmeter 使用一样方便,又支持 python 语法的测试框架,怎么办,那我们开发一个吧。
本篇博客主要讲测试框架的使用。下一篇博客再来解析源码。
可能会有人说,你吹牛逼吧,吹不吹牛逼,我们先看了再说吧。
为了让测试小伙伴只做纯脚本编写,因此本项目是一个web 项目。后端是我开发的,前端是我们公司前端开发小伙伴庄老板开发。话不多说,来看看框架如何使用。
测试框架搭建
后端项目需要注意包的引入。在 lz_test 项目resources目录下use_package,这个目录下放着两个包,到时候导入到你们公司的私服就可以了,导入私服方法如下
-
导入私服
-
从私服中拷贝
-
引入到项目
前端项目
前端项目可能需要解决一些包的引入问题,到时候你根据实际情况install 相关包即可,本次用到的4个项目git 地址放到文章的最后。感兴趣的可以下载下来自行研究。
测试框架的使用
-
登陆,默认用户名: admin ,密码: admin
-
根据不同的项目创建不同的测试用例
-
新建测试框架页面
-
选择【普通页面】类型,再选择测试接口页面
-
可以看到在测试页面中有组和接口,可以添加多个组,每个组下可以选择不同的接口。
-
点击添加组,填写组名称即可
-
为组添加接口
-
新增接口
-
接口编写
-
脚本大部分语法是 python的,只有小部分是java的
-
话不多说,直接来测试每一项语法吧,如果用户想添加新的方法,可以直接在 testshell项目的 methods 包下,任意类中添加任意方法,即可直接在脚本中使用。
如下面即将使用 math函数相关方法,java的实现是在MathMethod 这个类中提前定义好方法。即可直接在脚本中使用。
-
math 函数支持,目前主要是对 python 中的 Math.xxx()函数的支持,支持了一部分方法,其他方法,可能觉得没有什么用处,因此就不提供支持了,如果想自己添加扩展方法,直接在 testshell 项目下methods包下的任意类添加你想要的方法即可。
-
map操作支持,map 是关键字,我们不能直接用 map 作为变量名或方法名
-
空对象及空串处理
-
list 定义及使用
list 按道理和 python 中的 list 的使用方法一样,在脚本语言中不支持元组,因为我觉得元组和 list的使用方式极其相似,因此我觉得元组的使用,可能会增加测试人员的负担,因此不提提供支持。但是 list 中集成的元组的功能,如 a,b = list ,则将 list[0]赋值给a,list[1:]后面所有的值赋值给 b ,如果 list 只有两个元素,则将 a=list[0],b = list[1]。同时 list 遍历也延用了 python 的语法。
小伙伴可以看到在 python 中 list 遍历的好处,如果我既想得到遍历的索引,又想得到遍历的值,在 java中有两种写法。
java 中的写法
for(int i = 0 ;i < list.size();i ++){
item = list.get(i);
print(i,item)
}
或
int i = 0 ;
for(item in list ){
print(i,item)
i ++
}
python 中的写法
for(i,item in list ){
print(i,item)
}
聪明的小伙伴肯定一眼就看出来,python 比 java 好用,因为在 java 中无论怎样写,都是"鱼和熊掌不可兼得",要么就得到list 中的 item,要么就得到 i,不能同时得到,但是在 python 中就可以直接得到了。接下来我们来看看自定义框架中对于 string 的支持 -
对于 string 的支持,对于 string 的支持也是延用 python 的语法,如取倒数第二个字符a = ‘abcdef’, 直接使用 a[-2]即可,如想截取从第2到第4个字符 ,包含第二个字符 ,不包含第4个字符,直接a[2:4]即可,当然字符下标是从0开始的。
-
三目运算符
三目运算符我使用的是 java 的语法,因为 python 的语法确实有点晕。我们来看看 python 语法。
代码中需要这样用,我是 java出身的,觉得这样写确实有点诡异,因此,还是用 java 的三目运算符吧,如 a 大于b ,取 a ,否则取 b , 写成 a > b ? a : b ,更加顺口而已。 -
时间处理
上面需要注意的是,time()函数返回的是一个number 类型的毫秒值,date()返回的是 Date 类型,日期转毫秒,直接调用日期的 getTime()方法即可,毫秒值转日期,直接调用 date()方法即可,传入时间的毫秒值。就可以将number类型转化为日期类型。 -
for 循环语法,在之前的 list 集合使用时,我们己经使用过 for 的一些语法,下面我们继续来探索 for循环语法的使用。
对于 for我一的使用,一部分是延用 java 的语法,一部分是用了 python 的语法,当需要退出多层循环时,可以在最外层 for 循环之上定义一个 label_标签,当内部条件不满足时,可以用 break label_来终止多层循环,而对于list 和 map 的遍历,我觉得还是 python 语法好用。 -
while循环,及 do while 使用
可以支持 while(){},while(){ swith() { case : ; default : break ; } } ,do {} while(condition) 语法。这些语法延用 java 语法。可以自己去测试一下。 -
return 语句使用,没有什么好说的,和 python 一样
-
异常处理,当异常没有被捕获时,程序终止
当异常被捕获时,即使程序抛出异常,程序也不会被终止,会继续向下执行。
-
加密方法,当前支持sha256加密,和md5加密
-
方法参数指定默认值
-
方法作为方法参数使用,如下图中,max 方法作为findMax方法的第三个参数使用,这是 java 中没有的使用方式,在 python中很常见。
-
方法内嵌套方法,方法返回值是方法的引用,在外层使用方法引用调用方法,此语法和 python语法一样
-
强大的柯里化
-
利用柯里化特性,使用 logger方法打印方法的请求参数及返回参数
-
内层方法修改外层方法变量,无效
-
方法局部定义的变量不能修改全局变量
-
方法局部变量list集合使用
-
方法参数默认值设置,在下图中可以看到,如果只传一个参数,默认情况下,是填充方法的第一个参数,当然也可以像 foo3一样,你什么都不传。
-
通过外部传参数的方式覆盖掉方法的局部参数(在list或 map为参数的情况)
-
继承python语法,* args 和 * kwargs 的使用
- * args 和 **kwargs应用场景,打印方法的请求参数和返回参数。
36. 基于注解方式实现,打印方法请求参数返回值
37. 柯里化的复杂嵌套使用
-
柯里化使用场景,根据方法上配置注解的参数,打印方法是否超时,在下图中可以看到add1方法容忍超时时间为2秒,add2方法容忍超时时间为3秒,在 add1方法内睡眠1秒,add2方法内睡眠3秒,在方法的执行过程中,打印出 add2方法超时。
-
使用偷天换日的方式替换掉原来方法,根据柯里化方法规则,我们知道src代表的是 b 方法,dest代表的是目标方法 add,根据实际业务逻辑,可以替换掉原方法。
-
方法柯里化属性本来是没有的,可以后天获得
-
柯里化复杂使用场景,当柯里化中嵌套柯里化时,为每一层方法打印日志。
执行结果
-
lambda表达式简单使用场景
-
lambda和list结合使用场景
-
lambda表达式作为方法参数使用
-
lambda和map关键字结合使用
map 关键字的语法和 python 的语法是一致的。
-
脚本最基本的用法 ,+ ,- ,* ,/ , i++,i–,--i,++i,%= ,<<= ,>>>= 等
运行结果
-
线上测试用例, 登陆接口,用户通过登陆获取用户名和密码,获取 token,并定义全局 token,在一个组内,任何其他的接口都可以通过 import来导入 token变量
-
导入token 变量,并通过 token获取用户信息
获取用户信息脚本
测试结果
-
为了不污染变量,可以用 export关键字导出变量,导出的变量只为下一个接口使用,这个什么意思呢?如定义的全局变量token,这个测试组中所有的接口中都可以import 导入token变量,但有时候,变量不需要定义成全局变量,只为当前接的下一个接口使用,而其他接口不需要使用,可以使用 export 关键字来导出变量。话不多说,先来测试一把。
a)在第一个接口中定义变量 a
b) 在第二个接口中导入变量 a
c)第三个接口中导入变量a
测试结果,在第2个接口中能拿到定义的变量a,在第三个接口中拿不到变量 a
在这里插入图片描述
-
批量测试
-
当某个组中的某个接口抛出异常时,后面的接口停止执行
新增一个异常接口
测试
-
自定义方法
在第二个接口中引入mysum.tsh文件中的 mysum方法
上述做法的好处,就是不限定测试小伙伴想象,小伙伴可以利用语法开发出好用的公共方法。 -
文件冲突解决,新增接口不会出现文件冲突的,只有在编辑文件时,会出现,如,用户A编辑A接口,用户B也编辑 A接口,用户A 修改接口文件提交,用户B 再修改接口提交,这个时候可能会出现冲突,用户 B 解决冲突,再提交代码,此时接口 A 得到的将是 A 用户和 B 用户合并后的代码。
admin 用户打开gittest12接口 ,新增内容aaaaaaaaaaaaaaaaaa
zc 用户打开gittest12,新增内容 bbbbbbbbbbbbbbbbbbbb
提示代码冲突
解决冲突,点击保存
编辑的不是同一块代码,代码自动合并情况
git操作代码的源码如下
//需要注意的是每一个用户名都会有一个自己分支,每次都切换到自己的分支,将编辑的代码和 master 代码合并,如果 //冲突,冲突的代码返回给前端,前端解决冲突,即可保存代码,并提交,再合并到 master 中,再提交远程 public R gitOperation(String branch, String directory, String fileName, String code) { try { // 拉取 master 代码 , 恢复 master 代码 String currBranch = git.getBranch(); //获取当前本地分支 if (MASTER.equals(currBranch)) { // 如果当前分支是 master分支 git.fetch(); git.reset(MASTER); git.add(); git.commit("提交修改");//为了保险起见,还是 git -am "提交修改" 一下 git.checkout(branch); //切换到用户自己分支 git.pull(); } else if (branch.equals(currBranch)) { //如果当前分支是自己之前所在分支,可能用户正在解决冲突 //... //什么都不做 } else { // 如果是在别人的账户下,可能别人的代码也冲突了,因此需要恢复别人的代码,再切换到自己所在分支 git.add(); git.commit("切换到" + branch + "分支,先提交代码"); git.checkout(branch); git.pull(); } git.write(directory, fileName, code); // 写入文件 git.add(directory + "/" + fileName); // 提交推送到远程 git.commit("提交本次修改"); String status = git.merge(MASTER); // 合并 master 文件 if ("Conflicting".equals(status)) { String content = git.read(directory, fileName); R r = R.error("代码冲突").putData("code", content); r.put("code", 300); return r; } git.push(branch); //如果代码没有冲突,将分支代码推送到远程 git.checkout(MASTER); //切换到 master分支 git.pull(); git.merge(branch); //合并分支代码 git.push(MASTER); //推送master到远程 return R.ok(); } catch (Exception e) { log.error("git 操作文件异常", e); } return R.error("git操作异常"); }
上述代码的意思是什么呢?先来看第一种情况,如用户 A 编辑接口1,先创建并切换到A分支,修改接口1的文件内容,提交代码,再合并 Master 分支代码,如果没有冲突,则推送当前分支代码到远程,切换到 Master 创建,合并 A分支代码,推送Master 分支代码到远程。
当 A,B 两个用户同时来编辑接口1时,又是怎样的情景呢?A 用户打开接口1,同时 B 用户也打开接口1,A 用户编辑并保存,流程和第一种情况一样,B 用户编辑保存时,Master分支中己经有 A 用户的编辑的代码了,如果 A 用户和 B 用户编辑的是同一块代码,先切换到 B 分支,保存B 用户编辑的代码,合并 Master,此时文件冲突,将冲突的代码返回给前端,用户在前端页面解决冲突,再次保存,此时合并成功。
还有第三种情况,当 A,B 两个用户同时来编辑接口1时,又是怎样的情景呢?A 用户打开接口1,同时 B 用户也打开接口1,A 用户编辑并保存,流程和第一种情况一样,B 用户编辑保存时,Master分支中己经有 A 用户的编辑的代码了,如果 A 用户和 B 用户编辑的不是同一块代码,先切换到 B 分支,保存B 用户编辑的代码,合并 Master,此时文件合并成功。
在使用过程中需要注意的是修改你的 gitlab 的用户名和密码及项目地址
关键源码解析
@Test public void test() throws Exception { ClassLoader classLoader = TClassUtils.getDefaultClassLoader(); URL url = classLoader.getResource("code/base.tsh"); String path = url.getPath(); int a = path.lastIndexOf("/"); path = path.substring(0, a); ResouceHelp resouceHelp = new ResouceHelp(); Map<String, Object> init = new LinkedHashMap<>(); //主要是加载base.tsh 文件中定义的公共方法,v如 http 请求相关的 open 方法和文件上传的 upload方法 for (File file : TFileUtils.getFiles(path)) { String content = TFileUtils.readToStr(file.getPath()); TTuple1<Map<String, Object>> baseVarible = Utils.run(content, null, null, null, resouceHelp).getData(); init.putAll(baseVarible.getFirst()); } //global主要是存储全局变量,也就是 global 定义的变量 Map<String, Object> globals = new LinkedHashMap<>(); //import 主要是存储在接口代码中 export 关键字导出的变量及值 Map<String, Object> imports = new LinkedHashMap<>(); //依次执行三个接口 List<String> files = Arrays.asList(new String[]{ "/Users/quyixiao/git/java-python/script/yijie/get_verify_code.tsh", "/Users/quyixiao/git/java-python/script/yijie/login.tsh", "/Users/quyixiao/git/java-python/script/yijie/test.tsh" }); TTuple3<Map<String, Object>, Map<String, Object>, Map<String, Object>> data = null; for (String f : files) { String content = TFileUtils.readToStr(f); //每次将global 定义的变量及export导出的变量及值设置到下一个接口调用的名称空间内 data = Utils.run(content, init, globals, imports, resouceHelp).getData(); globals = data.getSecond(); imports = data.getThird(); } }
test 测试用例需要注意的是 run 方法,关于 run 方法如何实现,我们在下一篇博客中再来深究了。
下一篇博客将对整个脚本解析器源码进行解析,目前因为工作上事情也多,可能写得没有那么及时,不过,是肯定会写出来的,感兴趣的小伙伴请耐心等待。
总结 :
经过这段时间的学习,我相信大家对测试框架的使用己经有了一定了解,目前这个框架己经开始在公司中推广,虽然有一些小 bug ,但是不影响使用,后面这些 bug 修复好以后,我再来更新代码了。
注意:
因为只用了两个星期开发这个脚本解析器,目前只对python的部分语法支持,如 python中的类,魔数等,都没有提供支持,但我觉得当前脚本支持语法己经足够使用,将来如果需要其他功能,大家可以给我留言,或者根据公司业务需求,从 github clone代码自行开发。
本项目中下面两个包可能我随时更新新的代码,还是希望用户自己去下载testshell编译,源码地址文档的最后。
本次测试所有的接口脚本都放到了项目的 sql文件下。
git 地址
脚本解析器源码
https://github.com/quyixiao/testshell.git
mybatis 小插件源码
https://github.com/quyixiao/lz_mybatis_plugin