教你快速打造一个可视化接口自动化测试系统

现现在,接口开发几乎成为一个互联网公司的标配了,不管是web仍是app,哪怕是小程序,都离不开接口做为支撑,固然,这里的接口范围很广,从http到websocket,再到rpc,只要能实现数据通讯的均可以称之为接口,面临着如此庞大的接口数据,若是更好的管理和测试他们都是一个比较头疼的问题,更主要的是不少业务场景是须要多个接口进行联调的,所以在接口开发完成后,一轮自动化测试能快速反馈出当前系统的情况,面对这样的需求,一个对测试人员友好的可视化接口自动化测试系统就显得必不可少了。那么,咱们今天就来和你们聊聊如何实现一个小型的http接口自动化测试系统!javascript

咱们拿DOClever 作为这套系统的范本进行阐述,由于它是开源的,源码随时能够从GitHub和OSChina上获取,同时,这套系统内置了完整的自动化测试框架,从无需一行代码的UI测试用例编写,到更强大更灵活的代码模式,都提供了很友好的支持。html

系统需求:
  1. 能在一个测试用例里能够对一个接口自由编辑其入参,运行并判断出参是否正确,同时能够查看该接口完整的输入输出数据前端

  2. 能在一个测试用例里能够对一组接口进行测试,自由调整他们的执行顺序,并根据上一接口的出参做为下一接口的入参条件。vue

  3. 能实现基本的逻辑判断,好比if,elseif,同时能够自定义变量用于存储临时值,而且定义当前用例的返回值。java

  4. 提供一组辅助工具,能够快速实现数据打印,断言,用户输入,文件上传等操做。node

  5. 能在一个测试用例里嵌入其余的测试用例,并自由对其测试用例传参,获取返回值来实现数据上的联动es6

  6. 当用户输入时,能够实现快速提示,自动完成,让用例的编辑更友好!web

准备条件:

1.咱们采用nodejs+mongodb的架构设计,node端采用express框架,固然你也能够根据你的喜爱选择koa或者其余框架面试

2.前端咱们采用vue+elementUI来实现展现,这样作无非是为了数据的快速响应和element提供丰富的UI支持来帮助咱们快速搭建可视化页面。ajax

架构设计:

先给出一张自动化测试的动态图:

那么,咱们首先就从最基层的代理服务端来讲起若是对接口数据进行转发。

所谓的接口数据转发无非就是用node作一层代理中转,好在node其实很擅长作这样的工做,咱们把每一次的接口请求都看做是对代理服务端的一次post请求,接口的真实请求数据就直接做为post请求数据发给代理服务器,接口的host,path,method等数据都会包装在post请求的http header里面,而后咱们用node的stream直接pipe到真实请求上去,在接受到真实的接口返回数据后,会把这个数据pipe到原先post请求的response上面去,这样就完成了一次代理转发。

有几点须要注意的是:

1.你在发送请求前须要判断当前的请求是http仍是https,由于这涉及到两个不一样的node库。

2.你在转发真实请求前,须要对post过来的http header进行一次过滤,过滤掉host,origin等信息,保留客户须要请求的自定义头部和cookies.

3.不少时候,接口返回的多是一个跳转,那么咱们就须要处理这个跳转,再次请求这个跳转地址并接受返回数据.

4.咱们须要对接口返回过来的数据进行一个一次过滤,重点是cookie,咱们须要处理set-cookie这个字段,去掉浏览器不可写的部分,这样才能保证咱们调用登录接口的时候,能够在本地写入正确的cookie,让浏览器记住当前的登录状态!

5.咱们用一个doclever-request自定义头部来记录一次接口请求的完整request和response过程!

下面是实现的核心代码,在此列举出来:

var onProxy = function (req, res) { counter++; var num = counter; var bHttps=false; if(req.headers["url-doclever"].toLowerCase().startsWith("https://")) { bHttps=true; } var opt,request; if(bHttps) { opt= { host: getHost(req), path: req.headers["path-doclever"], method: req.headers["method-doclever"], headers: getHeader(req), port:getPort(req), rejectUnauthorized: false, requestCert: true, }; request=https.request; } else { opt= { host: getHost(req), path: req.headers["path-doclever"], method: req.headers["method-doclever"], headers: getHeader(req), port:getPort(req) }; request=http.request; } var req2 = request(opt, function (res2) { if(res2.statusCode==302) { handleCookieIfNecessary(opt,res2.headers); redirect(res,bHttps,opt,res2.headers.location) } else { var resHeader=filterResHeader(res2.headers) resHeader["doclever-request"]=JSON.stringify(handleSelfCookie(req2)); res.writeHead(res2.statusCode, resHeader); res2.pipe(res); res2.on('end', function () { }); } }); if (/POST|PUT|PATCH/i.test(req.method)) { req.pipe(req2); } else { req2.end(); } req2.on('error', function (err) { res.end(err.stack); }); };

给你们截取一个向代理服务器发送post请求的数据截图:

 

能够看到在request headers里面headers-doclever,methos-doclever,path-doclever,url-doclever都表明了真实接口的请求基本数据信息。而在request payload里面则是真实请求的请求体。

那么,咱们顺着请求分发往上走,先来看看整个自动化测试的最上层,也就是h5可视化界面的搭建(核心部分留到最后再说)。

先给各位上个图:

 

ok,看起来界面并不复杂,我先来讲下大概的思路。

  1. 上图中每个按钮均可以生成一个测试节点,好比我点击接口,就会插入一个接口在图上的下半部分显示,每个节点都有本身的数据格式。

  2. 每个节点都会生成一个ID,表明这个节点的惟一标识,咱们能够拖拽节点改变节点的位置,可是ID是不变的。

当咱们点击运行按钮的时候,系统会根据当前的节点顺序生成伪代码。

 

 

上图生成的伪代码就是

var $0=await 获取培训列表数据({param:{},query:{},header:{},body:{},}); log("打印log:"); var $2=await 每天(...[true,"11",]); var $3=await ffcv({param:{},query:{},header:{aa:Number("3df55"),gg:"",},body:{},}); var $4=await mm(...[]);

上图中蓝色部分就是须要测试的接口,而橘黄色就是嵌入的其余用例,咱们能够看到接口的运行咱们是能够传入咱们自定义的入参的,param,query,header和body的含义我相信大伙都能明白,而用例的传参咱们则是用了es6的一个语法参数展开符来实现,这样就能够把一个数组展开成参数,在这里有几点要说明的:

  1. 由于不管是接口仍是用例执行的都是一个异步调用的过程,因此咱们在这里须要用await来等待异步的执行完成(这也决定了该系统只能运行在支持es6的现代浏览器上)

  2. 那些蓝色和橘黄色文字的本质是什么呢,在这里是一个html的link标签,在后面会被转换成一个函数闭包(后面会详细解释)

         3.关于上下接口数据的关联,由于每一个节点都有惟一的ID,这里0变量表明的就是获取培训列表数据,因此在后面的代码里,咱们即可以用这个变量来引用这个接口数据,好比0.data.username表明的就是获取培训列表数据这个接口返回数据里面的username这个字段的值。

OK,咱们回到咱们以前的话题上面来,如何在可视化界面上生成这些测试节点呢,好比咱们点击按钮,会发生哪些事情呢。

  1. 首先咱们点击接口按钮,会弹出一个选择框让咱们选择接口信息,这里的接口数据采集你们能够自定义,选择本身喜欢的格式就行,以下图:
  1. 点击保存后,接口的数据会被以JSON的格式存储在测试节点中,大体格式以下:
{ type:"interface", id:id, name: "info",   //接口名称 data:JSON.stringify(obj), //obj就是接口的json数据 argv:{ //这里是外界的接口入参,也就是上图中被转换成伪代码的接口入参部分 param:{}, query:{}, header:{}, body:{} }, status:0, //当前接口的运行状态 modify:0 //接口数据是否被修改 }

3.而后咱们用一个array存储这个节点信息,在vue里面用一个v-for加上el-row就能够将这些节点展示出来。
那么如何去决定一个测试用例的是否测试经过呢,咱们这里会用到测试用例的返回值,以下图所示:

 

 

未断定就是表示当前用例执行结果未知,经过就是用例经过,不经过就是用例不经过,同时,咱们还能够定义返回参数。该节点生成的数据结构以下:

{ type:"return", id:_this.getNewId(), //获取新的ID name:(ret=="true"?"经过":(ret=="false"?"不经过":"未断定")), data:ret, //true:经过,false:未经过 undefined:未断定 argv:argv //返回参数 }

全部节点的完整数据结构信息能够参考GitHub和OSChina里面的源代码
好的,咱们继续往下说,当咱们点击运行按钮的时候,测试节点会被转换成伪代码,这一块比较好理解,好比接口节点就会根据数据结构信息转换成
var $0=await 获取培训列表数据({param:{},query:{},header:{},body:{},});
这样的形式,核心转换代码以下:


helper.convertToCode=function (data) { var str=""; data.forEach(function (obj) { if(obj.type=="interface") { var argv="{"; for(var key in obj.argv) { argv+=key+":{"; for(var key1 in obj.argv[key]) { argv+=key1+":"+obj.argv[key][key1]+"," } argv+="}," } argv+="}" str+=`<div class='testCodeLine'>var $${obj.id}=await <a href='javascript:void(0)' style='cursor: pointer; text-decoration: none;' type='1' varid='${obj.id}' data='${obj.data.replace(/\'/g,"&apos;")}'>${obj.name}</a>(${argv});</div>` } else if(obj.type=="test") { var argv="["; obj.argv.forEach(function (obj) { argv+=obj+"," }) argv+="]"; str+=`<div class='testCodeLine'>var $${obj.id}=await <a type='2' href='javascript:void(0)' style='cursor: pointer; text-decoration: none;color:orange' varid='${obj.id}' data='${obj.data}' mode='${obj.mode}'>${obj.name}</a>(...${argv});</div>` } else if(obj.type=="ifbegin") { str+=`<div class='testCodeLine'>if(${obj.data}){</div>` } else if(obj.type=="elseif") { str+=`<div class='testCodeLine'>}else if(${obj.data}){</div>` } else if(obj.type=="else") { str+=`<div class='testCodeLine'>}else{</div>` } else if(obj.type=="ifend") { str+=`<div class='testCodeLine'>}</div>` } else if(obj.type=="var") { if(obj.global) { str+=`<div class='testCodeLine'>global["${obj.name}"]=${obj.data};</div>` } else { str+=`<div class='testCodeLine'>var ${obj.name}=${obj.data};</div>` } } else if(obj.type=="return") { if(obj.argv.length>0) { var argv=obj.argv.join(","); str+=`<div class='testCodeLine'>return [${obj.data},${argv}];</div>` } else { str+=`<div class='testCodeLine'>return ${obj.data};</div>` } } else if(obj.type=="log") { str+=`<div class='testCodeLine'>log("打印${obj.name}:");log((${obj.data}));</div>` } else if(obj.type=="input") { str+=`<div class='testCodeLine'>var $${obj.id}=await input("${obj.name}",${obj.data});</div>` } else if(obj.type=="baseurl") { str+=`<div class='testCodeLine'>opt["baseUrl"]=${obj.data};</div>` } else if(obj.type=="assert") { str+=`<div class='testCodeLine'>if(${obj.data}){</div><div class='testCodeLine'>__assert(true,${obj.id},"${obj.name}");${obj.pass?"return true;":""}</div><div class='testCodeLine'>}</div><div class='testCodeLine'>else{</div><div class='testCodeLine'>__assert(false,${obj.id},"${obj.name}");</div><div class='testCodeLine'>return false;</div><div class='testCodeLine'>}</div>` } }) return str; }

能够看到,上面的代码把每一个测试节点就转换成了html的节点,这样既能够在网页上直接展现,也方便接下来的解析成真正的javascript可执行代码。
好,接下来咱们进入整个系统最核心,最复杂的部分,如何把上述的伪代码转换成可执行代码去请求真实的接口,并将接口的状态和信息返回的呢!
咱们先来用一张表表示下这个过程:

 

 

若是对软件测试、接口测试、自动化测试、面试经验交流。感兴趣能够加软件测试交流:1085991341,还会有同行一块儿技术交流。
咱们一个个步骤来看下:

1.对转换后的html节点进行解析,将接口和测试用例的link节点替换成函数闭包,基本代码表示以下:

var ele=document.createElement("div"); ele.innerHTML=code;      //将html的伪代码赋值到新节点的innerHTML中 var arr=ele.getElementsByTagName("a"); //获取当前全部接口和用例节点 var arrNode=[]; for(var i=0;i<arr.length;i++) { var obj=arr[i].getAttribute("data");  //获取接口和用例的json数据 var type=arr[i].getAttribute("type"); //获取类型:1.接口 2.用例 var objId=arr[i].getAttribute("varid"); //获取接口或者用例在可视化节点中的ID var text; if(type=="1")     //节点 { var objInfo={}; var o=JSON.parse(obj.replace(/\r|\n/g,"")); var query={ project:o.project._id } if(o.version) { query.version=o.version; } objInfo=await 请求当前的接口数据信息并和本地接口入参进行合并; opt.baseUrls=objInfo.baseUrls; opt.before=objInfo.before; opt.after=objInfo.after; text="(function (opt1) {return helper.runTest("+obj.replace(/\r|\n/g,"")+",opt,test,root,opt1,"+(level==0?objId:undefined)+")})"   //生成函数闭包,等待调用 } else if(type=="2")   //为用例 { 代码略 } var node=document.createTextNode(text); arrNode.push({ oldNode:arr[i], newNode:node }); } //将转换后的新text节点替换原来的link节点 arrNode.forEach(function (obj) { if(obj) { obj.oldNode.parentNode.replaceChild(obj.newNode,obj.oldNode); } })

2.获得完整的执行代码后,如何去请求接口呢,咱们来看下runTest函数里面的基本信息:

helper.runTest=async function (obj,global,test,root,opt,id) { root.output+="开始运行接口:"+obj.name+"<br>"
    if(id!=undefined) { window.vueObj.$store.state.event.$emit("testRunStatus","interfaceStart",id); } var name=obj.name var method=obj.method; var baseUrl=obj.baseUrl=="defaultUrl"?global.baseUrl:obj.baseUrl; /** 这里的代码略,是对接口数据的param,query,header,body数据进行填充 **/ var startDate=new Date(); var func=window.apiNode.net(method,baseUrl+path,header,body);  // 这里就是网络请求部分,根据你的喜爱选择ajax库,我这里用的是vue-resource return func.then(function (result) { var res={ req:{ param:param, query:reqQuery, header:filterHeader(Object.assign({},header,objHeaders)), body:reqBody, info:result.header["doclever-request"]?JSON.parse(result.header["doclever-request"]):{} } }; res.header=result.header; res.status=String(result.status); res.second=(((new Date())-startDate)/1000).toFixed(3); res.type=typeof (result.data); res.data=result.data; if(id!=undefined) { if(result.status>=200 && result.status<300) { window.vueObj.$store.state.event.$emit("testRunStatus","interfaceSuccess",id,res);  //这里就会将接口的运行状态传递到前端可视化节点中 } else { window.vueObj.$store.state.event.$emit("testRunStatus","interfaceFail",id,res); } } root.output+="结束运行接口:"+obj.name+"(耗时:<span style='color: green'>"+res.second+"秒</span>)<br>"
return res; })

3.最后咱们来看下如何执行整个js代码,并对测试用例进行返回的:

var ret=eval("(async function () {"+ele.innerText+"})()").then(function (ret) { //这里执行的就是刚才转换后真实的javascript可执行代码 var obj={ argv:[] }; var temp; if(typeof(ret)=="object" && (ret instanceof Array)) { temp=ret[0]; obj.argv=ret.slice(1); } else { temp=ret; } if(temp===undefined) { obj.pass=undefined; test.status=0; if(__id!=undefined) { root.unknown++; window.vueObj.$store.state.event.$emit("testRunStatus","testUnknown",__id);   //将当前用例的执行状态传递到前端可视化节点上去 window.vueObj.$store.state.event.$emit("testCollectionRun",__id,root.output.substr(startOutputIndex),Date.now()-startTime); } root.output+="用例执行结束:"+test.name+"(未断定)"; } else if(Boolean(temp)==true) { obj.pass=true; test.status=1; if(__id!=undefined) { root.success++; window.vueObj.$store.state.event.$emit("testRunStatus","testSuccess",__id); window.vueObj.$store.state.event.$emit("testCollectionRun",__id,root.output.substr(startOutputIndex),Date.now()-startTime); } root.output+="用例执行结束:"+test.name+"(<span style='color:green'>已经过</span>)"; } else { obj.pass=false; test.status=2; if(__id!=undefined) { root.fail++; window.vueObj.$store.state.event.$emit("testRunStatus","testFail",__id); window.vueObj.$store.state.event.$emit("testCollectionRun",__id,root.output.substr(startOutputIndex),Date.now()-startTime); } root.output+="用例执行结束:"+test.name+"(<span style='color:red'>未经过</span>)"; } root.output+="</div><br>"
    return obj; });

好的,大致上咱们这个可视化的接口自动化测试平台算是完成了,可是这里面涉及到细节很是多,我大体列举下:1.eval是不安全的,如何让浏览器端安全的执行js代码呢2.若是遇到须要文件上传的接口,须要怎么去作呢3.既然能够在前端自动化测试,那么我可不能够把这些测试用例放到服务端而后自动轮询呢以上就是本文的所有内容,但愿对你们的学习有所帮助。有被帮助到的朋友欢迎点赞,评论。

相关文章
相关标签/搜索