上一节介绍到了使用groovy实现接口自动化测试的基本功能。
本节将介绍groovy执行用例动态参数、参数回传、参数加密、soapui引入第三方jar包、生成随机参数(绕过业务逻辑中的一些唯一校验阻碍自动化),以适应更真实、复杂的场景。
soapui引入第三方jar包
现在JAVA的优势在于JVM平台和累积起来的丰富的第三方资源了,这也是groovy类JVM语言的优势。
将第三方jar包拷贝只<soapui安装目录>/bin/ext
目录下即可。
然后代码中使用import引入:例import customer.RSAUtils
加密
本例使用RSA加密,groovy需要使用java的第三方包,从网上找了个工具类打包到custom.jar,拷贝到soapui扩展目录。
1.生成公私密钥到d盘RSAUtils.generateKeyPair('d:/')
2.文本编辑器打开生成的publicKey.keystore
文件,将三行合并成一行用\n
分隔。例:
PUBLIC_KEY_BASE64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCC4d0q2qR7G21TmObv5l0fxpMHcD34pqCjJoIl\nvU/Oa+0rsNkkZljvQAenY8ZNpOPzcfUL/F+qwTpuJh5ny6zl9gPloQRd6PcWob1Z+cuSoEAwBZx4\n+Yw/2QAARjxs5e8GeF0IdY/HK/HmpTCKbmKbUxNaftmeRwfgaG/TGZ93CwIDAQAB"
3.修改任务文件,对账号密码进行加密。
{"comment": "登录接口 status=1 登录成功。reqName格式为:TestCaseName-TestStepName;expect为期望值;extParams的属性可以动态设置值并覆盖用例的默认参数","reqName": "TestSuite-login","extParams": {"account": "rsaEncrypt{lj745280746}","password": "rsaEncrypt{123456}"},"expect": {"status": "1"}
}
4.修改代码。任务文件新增了rsaEncrypt{<content>}
语法,需要在代码中解析。
//新增方法----------------------------
//rsa 加密内容
def rsaEncrypt(value) {return RSAUtils.encrypt(RSAUtils.getPublicKey(PUBLIC_KEY_BASE64), value)
}
//添加扩展参数
def addExtParams(params, testStep) {if (!params || params.size() == 0) returnparams.each {k, v ->testStep.setPropertyValue(k, interpreter(v))}
}
//解析替换字符串中的自定义语法
def interpreter(v) {def rsaParamPattern = ~/rsaEncrypt\{\s*(.*?)\s*\}/def rs = vdef sb = new StringBuffer() //rsa加密 替换 例:"rsaEncrypt{18600000000}"def mR = rsaParamPattern.matcher(rs)sb.delete(0, sb.length())while(mR.find()) {mR.appendReplacement(sb, rsaEncrypt(mR.group(1)))}mR.appendTail(sb)rs = sb.toString()
}
//······
//执行任务发送请求前覆盖默认参数
({ //加载任务def taskFile = new File(CURRENT_TESTCASE.properties.task_file.value)def tasks = JSON.parseText(taskFile.getText()) //执行任务tasks.each {def caseName = it.reqName.split("-")[0]def stepName = it.reqName.split("-")[1]def testStep = TEST_SUITE.getTestCaseByName(caseName).getTestStepByName(stepName) //发送请求前覆盖默认参数addExtParams(it.extParams, testStep)def testStepContext = new WsdlTestRunContext(testStep)def result = testStep.run(testRunner, testStepContext)log.info "【${caseName}-${stepName} result data】" + result.responseContentdef rsJson = JSON.parseText(result.responseContent)if (verifyExcept(rsJson, it.expect)) {log.info "【${it.reqName}】success!"assert true} else {log.error "【${it.reqName}】fail!"assert false}}
}())
后台查看请求数据,已经过加密:
参数回传
常有一些场景,接口B接收的参数依赖接口A返回的数据。典型的如打开一个创建页面调用A接口生成唯一的单号No
,录入信息后调用B接口保存数据。此时No
是动态的,必须临时存起来。
本教程举例:testA接口依赖login接口返回的token。修改任务文件,新增cache
属性,表示从返回值中获取cache
属性中的变量名,临时缓存起来,在后续请求中使用cache{<用例名>-<变量名>}
拿到缓存的值。代码中扩展interpreter
函数,使其支持该语法。(此处不贴代码了,在本节结尾提供完整代码。)
生成随机参数
实现该功能是为了解决一些场景会对输入信息做唯一性校验(如手机号),测试数据中如果写死手机号,第一次脚本执行成功,第二次校验接口将返回失败,因为同一个手机在第一次已经存进数据库。
例如testA接口的参数a需要唯一校验,按如下修改,然后扩展interpreter
函数,使其支持该语法(查看本节结尾提供完整代码)。
groovy完整代码及任务文件
现在已基本够用了(我够用了…),如上节所说还有些功能未实现,有兴趣请自行扩展。也许还有些bug。。。
后台接口代码、custom.jar、soapui项目文件,点击下载。
以下是groovy完整代码:
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestRunContext
import groovy.json.JsonSlurper
import customer.RSAUtilsJSON = new JsonSlurper()CURRENT_TESTCASE = testRunner.testCase
TEST_SUITE = CURRENT_TESTCASE.parent
//使用JAVA加密 必须导入customer.RSAUtils。customer.jar文件拷贝至目录:<soapui安装目录>\bin\ext 后重启soapui
PUBLIC_KEY_BASE64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCC4d0q2qR7G21TmObv5l0fxpMHcD34pqCjJoIl\nvU/Oa+0rsNkkZljvQAenY8ZNpOPzcfUL/F+qwTpuJh5ny6zl9gPloQRd6PcWob1Z+cuSoEAwBZx4\n+Yw/2QAARjxs5e8GeF0IdY/HK/HmpTCKbmKbUxNaftmeRwfgaG/TGZ93CwIDAQAB"
//缓存 后续需要回传的值
CACHE = [:]
//============================================= UTILS =======================================================
//递归 深度比较返回值与期望是否匹配
def verifyExcept(actual, expect) {def rs = true //类型判断 java Collection 与 Map 不同的遍历方式if (expect instanceof Collection) {expect.eachWithIndex {it, i -> //log.info "i:" + i + " it:" + itif (it instanceof Map || it instanceof Collection) {return rs = verifyExcept(actual[i], it)} else {def actualValue = actual != null ? actual[i] : nullif (actualValue != it) {log.error "expect: $index:${i}=${it} actual:${actualValue}"return rs = false}}}} else if (expect instanceof Map) {expect.each {k, v -> //log.info "k:" + k + " v:" + vif (v instanceof Map || v instanceof Collection) {return rs = verifyExcept(actual[k], v)} else {def actualValue = actual != null ? actual[k] : nullif (actualValue != v) {log.error "expect: ${k}=${v} actual:${actualValue}"return rs = false}}}} else {log.error "expect is not Object!"rs = false}return rs
}
//rsa 加密内容
def rsaEncrypt(value) {return RSAUtils.encrypt(RSAUtils.getPublicKey(PUBLIC_KEY_BASE64), value)
}
//添加扩展参数
def addExtParams(params, testStep) {if (!params || params.size() == 0) returnparams.each {k, v ->testStep.setPropertyValue(k, interpreter(v))}
}
//解析替换字符串中的自定义语法 正则替换有疑问请查java API
def interpreter(v) {def cachePattern = ~/cache\{\s*(.*?)(\[.+\])*\s*\}/def rsaParamPattern = ~/rsaEncrypt\{\s*(.*?)\s*\}/def randomPattern = ~/random\{\s*(.*?)\s*\}/def rs = vdef sb = new StringBuffer()def mC = cachePattern.matcher(v) //从缓存中取值 替换 例:"cache{login-token}"while(mC.find()) {if (mC.group(2)) { //使用Eval可以获取多层级的属性值 login-abc['a']['b']def tmp = Eval.me('CACHE', CACHE, "CACHE['${mC.group(1)}']${mC.group(2)}")mC.appendReplacement(sb, tmp)} else {mC.appendReplacement(sb, CACHE[mC.group(1)])}}mC.appendTail(sb)rs = sb.toString() //rsa加密 替换 例:"rsaEncrypt{18600000000}"def mR = rsaParamPattern.matcher(rs)sb.delete(0, sb.length())while(mR.find()) {mR.appendReplacement(sb, rsaEncrypt(mR.group(1)))}mR.appendTail(sb)rs = sb.toString() //生成随机数 替换 例:"soapui自动化-random{1000}"def mRd = randomPattern.matcher(rs)sb.delete(0, sb.length())while(mRd.find()) {mRd.appendReplacement(sb, Math.round(Math.random() * Integer.valueOf(mRd.group(1))) + "")}mRd.appendTail(sb)rs = sb.toString()return rs
}
//缓存需要的数据
def cacheData(items, rsData, prefix) {if (!items || items.size() == 0) return //默认在请求返回数据中取值 也可先缓存固定值共后续请求使用,使数据保持一致items.each {if (it instanceof Map) {it.each {k, v ->CACHE["${prefix}-${k}"] = v}} else {CACHE["${prefix}-${it}"] = rsData[it]}}
}
//============================================= RUN =======================================================
({ //加载任务def taskFile = new File(CURRENT_TESTCASE.properties.task_file.value)def tasks = JSON.parseText(taskFile.getText()) //执行任务tasks.each {def caseName = it.reqName.split("-")[0]def stepName = it.reqName.split("-")[1]def testStep = TEST_SUITE.getTestCaseByName(caseName).getTestStepByName(stepName) //发送请求前覆盖默认参数addExtParams(it.extParams, testStep)def testStepContext = new WsdlTestRunContext(testStep)def result = testStep.run(testRunner, testStepContext)log.info "【${caseName}-${stepName} result data】" + result.responseContentdef rsJson = JSON.parseText(result.responseContent)if (verifyExcept(rsJson, it.expect)) {cacheData(it.cache, rsJson, stepName)log.info "【${it.reqName}】success!"assert true} else {log.error "【${it.reqName}】fail!"assert false}}
}())return
任务文件:
[{"comment": "登录接口 status=1 登录成功。reqName格式为:TestCaseName-TestStepName;expect为期望值;extParams的属性可以动态设置值并覆盖用例的默认参数","reqName": "TestSuite-login","cache": ["token"],"extParams": {"account": "rsaEncrypt{lj745280746}","password": "rsaEncrypt{123456}"},"expect": {"status": "1"}
},{"comment": "测试接口A","reqName": "TestSuite-testA","extParams": {"token": "cache{login-token}","a": "test-random{10000}"},"expect": {"data": {"status": "1"}}
},{"comment": "测试接口B。未插入token,status=0","reqName": "TestSuite-testB","expect": {"data": {"status": "0"}}
}]