現現在,接口開發幾乎成爲一個互聯網公司的標配了,不管是web仍是app,哪怕是小程序,都離不開接口做爲支撐,固然,這裏的接口範圍很廣,從http到websocket,再到rpc,只要能實現數據通訊的均可以稱之爲接口,面臨着如此龐大的接口數據,若是更好的管理和測試他們都是一個比較頭疼的問題,更主要的是不少業務場景是須要多個接口進行聯調的,所以在接口開發完成後,一輪自動化測試能快速反饋出當前系統的情況,面對這樣的需求,一個對測試人員友好的可視化接口自動化測試系統就顯得必不可少了。那麼,咱們今天就來和你們聊聊如何實現一個小型的http接口自動化測試系統!javascript
咱們拿DOClever 作爲這套系統的範本進行闡述,由於它是開源的,源碼隨時能夠從GitHub和OSChina上獲取,同時,這套系統內置了完整的自動化測試框架,從無需一行代碼的UI測試用例編寫,到更強大更靈活的代碼模式,都提供了很友好的支持。html
系統需求:
-
能在一個測試用例裏能夠對一個接口自由編輯其入參,運行並判斷出參是否正確,同時能夠查看該接口完整的輸入輸出數據前端
-
能在一個測試用例裏能夠對一組接口進行測試,自由調整他們的執行順序,並根據上一接口的出參做爲下一接口的入參條件。vue
-
能實現基本的邏輯判斷,好比if,elseif,同時能夠自定義變量用於存儲臨時值,而且定義當前用例的返回值。java
-
提供一組輔助工具,能夠快速實現數據打印,斷言,用戶輸入,文件上傳等操做。node
-
能在一個測試用例裏嵌入其餘的測試用例,並自由對其測試用例傳參,獲取返回值來實現數據上的聯動es6
-
當用戶輸入時,能夠實現快速提示,自動完成,讓用例的編輯更友好!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,看起來界面並不複雜,我先來講下大概的思路。
-
上圖中每個按鈕均可以生成一個測試節點,好比我點擊接口,就會插入一個接口在圖上的下半部分顯示,每個節點都有本身的數據格式。
-
每個節點都會生成一個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的一個語法參數展開符來實現,這樣就能夠把一個數組展開成參數,在這裏有幾點要說明的:
-
由於不管是接口仍是用例執行的都是一個異步調用的過程,因此咱們在這裏須要用await來等待異步的執行完成(這也決定了該系統只能運行在支持es6的現代瀏覽器上)
-
那些藍色和橘黃色文字的本質是什麼呢,在這裏是一個html的link標籤,在後面會被轉換成一個函數閉包(後面會詳細解釋)
3.關於上下接口數據的關聯,由於每一個節點都有惟一的ID,這裏0.data.username表明的就是獲取培訓列表數據這個接口返回數據裏面的username這個字段的值。
OK,咱們回到咱們以前的話題上面來,如何在可視化界面上生成這些測試節點呢,好比咱們點擊按鈕,會發生哪些事情呢。
- 首先咱們點擊接口按鈕,會彈出一個選擇框讓咱們選擇接口信息,這裏的接口數據採集你們能夠自定義,選擇本身喜歡的格式就行,以下圖:

- 點擊保存後,接口的數據會被以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,"'")}'>${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.既然能夠在前端自動化測試,那麼我可不能夠把這些測試用例放到服務端而後自動輪詢呢以上就是本文的所有內容,但願對你們的學習有所幫助。有被幫助到的朋友歡迎點贊,評論。