教你快速打造一個可視化接口自動化測試系統


現如今,接口開發幾乎成為一個互聯網公司的標配了,無論是web還是app,哪怕是小程序,都離不開接口作為支撐,當然,這里的接口范圍很廣,從http到websocket,再到rpc,只要能實現數據通信的都可以稱之為接口,面臨着如此龐大的接口數據,如果更好的管理和測試他們都是一個比較頭疼的問題,更主要的是很多業務場景是需要多個接口進行聯調的,因此在接口開發完成后,一輪自動化測試能快速反饋出當前系統的狀況,面對這樣的需求,一個對測試人員友好的可視化接口自動化測試系統就顯得必不可少了。那么,我們今天就來和大家聊聊如何實現一個小型的http接口自動化測試系統!

我們拿DOClever 做為這套系統的范本進行闡述,因為它是開源的,源碼隨時可以從GitHub和OSChina上獲取,同時,這套系統內置了完整的自動化測試框架,從無需一行代碼的UI測試用例編寫,到更強大更靈活的代碼模式,都提供了很友好的支持。

系統需求:
  1. 能在一個測試用例里可以對一個接口自由編輯其入參,運行並判斷出參是否正確,同時可以查看該接口完整的輸入輸出數據

  2. 能在一個測試用例里可以對一組接口進行測試,自由調整他們的執行順序,並根據上一接口的出參作為下一接口的入參條件。

  3. 能實現基本的邏輯判斷,比如if,elseif,同時可以自定義變量用於存儲臨時值,並且定義當前用例的返回值。

  4. 提供一組輔助工具,可以快速實現數據打印,斷言,用戶輸入,文件上傳等操作。

  5. 能在一個測試用例里嵌入其他的測試用例,並自由對其測試用例傳參,獲取返回值來實現數據上的聯動

  6. 當用戶輸入時,可以實現快速提示,自動完成,讓用例的編輯更友好!

准備條件:

1.我們采用nodejs+mongodb的架構設計,node端采用express框架,當然你也可以根據你的喜好選擇koa或者其他框架

2.前端我們采用vue+elementUI來實現展示,這樣做無非是為了數據的快速響應和element提供豐富的UI支持來幫助我們快速搭建可視化頁面。

架構設計:

先給出一張自動化測試的動態圖:

那么,我們首先就從最基層的代理服務端來說起如果對接口數據進行轉發。

所謂的接口數據轉發無非就是用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.既然可以在前端自動化測試,那么我可不可以把這些測試用例放到服務端然后自動輪詢呢
以上就是本文的全部內容,希望對大家的學習有所幫助。有被幫助到的朋友歡迎點贊,評論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM