Teach you to quickly build a visual interface automated test system

Nowadays, interface development has almost become the standard configuration of an Internet company. Whether it is web or app, even small programs, it cannot be separated from the interface as support. Of course, the interface range here is very wide, from http to websocket to rpc, as long as it can realize data communication, it can be called an interface. Faced with such a huge interface data, if they are better managed and tested, they are a headache. The more important thing is that many business scenarios require multiple The interface is jointly debugged, so after the interface development is completed, a round of automated testing can quickly feedback the current system status. Facing such a demand, a visual interface automated testing system that is friendly to testers is essential. So, let’s talk to you today about how to implement a small http interface automated test system!

We use DOClever as a template for this system to explain, because it is open source, the source code can be obtained from GitHub and OSChina at any time. At the same time, this system has a built-in complete automated testing framework, which never requires a line of code for UI test cases. Compilation, to more powerful and flexible code mode, provides very friendly support.

system requirement:
  1. Can freely edit the input parameters of an interface in a test case, run and judge whether the parameters are correct, and view the complete input and output data of the interface

  2. Can test a group of interfaces in a test case, freely adjust their execution order, and use the output parameters of the previous interface as the input parameters of the next interface.

  3. It can realize basic logical judgments, such as if, elseif, and can customize variables to store temporary values ​​and define the return value of the current use case.

  4. Provides a set of auxiliary tools that can quickly implement data printing, assertion, user input, file upload and other operations.

  5. Can embed other test cases in a test case, and freely pass parameters to the test case, get the return value to realize the linkage on the data

  6. When the user enters, it can realize quick prompt and automatic completion, making the editing of use cases more friendly!

Preparation conditions:

1. We adopt the architecture design of nodejs+mongodb, and the node end adopts the express framework. Of course, you can also choose koa or other frameworks according to your preferences

2. At the front end, we use vue+elementUI to realize the display. This is nothing more than quick response to data and rich UI support for the element to help us quickly build visual pages.

Architecture design:

First give a dynamic picture of automated testing:

So, let's start with the most basic proxy server, how to forward interface data.

The so-called interface data forwarding is nothing more than using node as a layer of proxy relay. Fortunately, node is actually very good at doing this kind of work. We regard every interface request as a post request to the proxy server, the real request data of the interface. It is directly sent to the proxy server as the post request data. The host, path, method and other data of the interface will be packaged in the http header of the post request, and then we use the node's stream to pipe directly to the real request, and return it after receiving the real interface After the data is received, the data will be piped to the response of the original post request, thus completing a proxy forwarding.

There are a few things to note:

1. You need to determine whether the current request is http or https before sending the request, because this involves two different node libraries.

2. Before forwarding the real request, you need to filter the http header from the post once to filter out the host, origin and other information, and keep the custom headers and cookies that the customer needs to request.

3. Many times, the interface may return a jump, then we need to deal with the jump, request the jump address again and accept the returned data.

4. We need to filter the data returned by the interface once. The focus is on cookies. We need to deal with the set-cookie field and remove the unwritable part of the browser to ensure that we can write locally when we call the login interface. Enter the correct cookie to let the browser remember the current login status!

5. We use a doclever-request custom header to record the complete request and response process of an interface request!

The following is the core code of the implementation, listed here:

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);
    });
};

Let’s take a screenshot of the data of sending a post request to the proxy server:


You can see that headers-doclever, methos-doclever, path-doclever, url-doclever in request headers all represent the basic data information of the real interface request. In the request payload is the request body of the real request.

So, let's follow the request distribution and go up, let's take a look at the top layer of the entire automated test, which is the construction of the h5 visual interface (the core part is left to the last).

Let me give you a picture:


Ok, it seems that the interface is not complicated, let me first talk about the general idea.

  1. Each button in the above figure can generate a test node. For example, when I click on the interface, an interface will be inserted and displayed in the lower part of the figure. Each node has its own data format.

  2. Each node will generate an ID, which represents the unique identification of the node. We can drag the node to change the position of the node, but the ID is unchanged.

When we click the run button, the system will generate pseudo code according to the current node order.

The pseudo code generated in the above figure is

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(...[]);

The blue part in the above figure is the interface that needs to be tested, and the orange is the other embedded use cases. We can see that the operation of the interface can pass in our custom input parameters, the meaning of param, query, header and body I believe everyone can understand, and the parameter transfer of the use case is implemented by using a syntax parameter expansion operator of es6, so that an array can be expanded into parameters. Here are a few points to explain:

  1. Because both the interface and the use case are executed as an asynchronous call, we need to use await here to wait for the completion of the asynchronous execution (this also determines that the system can only run on modern browsers that support es6)

  2. What is the essence of the blue and orange text? Here is an html link tag, which will be converted into a function closure later (explained in detail later)

3. Regarding the association of the upper and lower interface data, because each node has a unique ID, the $0 variable here represents the acquisition of training list data, so in the following code, we can use this variable to refer to this interface data.

For example, $0.data.username represents the value of the username field in the data returned by the interface for obtaining training list data.

OK, let's return to our previous topic, how to generate these test nodes on the visual interface, for example, what happens when we click a button.

  1. First, we click on the interface button, and a selection box will pop up to let us choose the interface information. You can customize the interface data collection here and choose the format you like, as shown below:


2. After clicking Save, the data of the interface will be stored in the test node in JSON format. The approximate format is as follows:

{
    type:"interface",
    id:id,
    name: "info",   //接口名称
    data:JSON.stringify(obj),   //obj就是接口的json数据
    argv:{                //这里是外界的接口入参,也就是上图中被转换成伪代码的接口入参部分
        param:{},
        query:{},
        header:{},
        body:{}
    },
    status:0,   //当前接口的运行状态
    modify:0      //接口数据是否被修改
}

3. Then we use an array to store the node information, and use a v-for plus el-row in vue to display these nodes.
So how to determine whether a test case passed the test, we will use the return value of the test case here, as shown in the following figure:

Undecided means that the current use case execution result is unknown. Pass is the use case passed, and if it fails, the use case fails. At the same time, we can also define return parameters. The data structure generated by this node is as follows:

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

For the complete data structure information of all nodes, please refer to the source code in GitHub and OSChina.
Well, let’s go on to say that when we click the run button, the test node will be converted into pseudo code. This one is easier to understand, such as the interface. The node will be converted to
var $0=await according to the data structure information to obtain the training list data ({param:{},query:{},header:{},body:{},}); in
this form, the core conversion code is as follows :

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;
}

As you can see, the above code converts each test node into an html node, so that it can be displayed directly on the web page, and it is also convenient for subsequent parsing into real javascript executable code.
Ok, next we enter the core and most complex part of the whole system, how to convert the above pseudo code into executable code to request the real interface, and return the status and information of the interface!
Let us first use a table to show this process:

how to exchange experience in software testing, interface testing, automated testing, and interviews. If you are interested, you can add software test communication: 1085991341, and there will be technical exchanges with colleagues.
Let's look at the steps one by one:

1. Analyze the converted html node and replace the link node of the interface and test case with a function closure. The basic code is as follows:

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. After getting the complete execution code, how to request the interface? Let's look at the basic information in the runTest function:

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. Finally, let's take a look at how to execute the entire js code and return the test case:

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;
});

Okay, in general our visual interface automation test platform is complete, but there are many details involved, I will give a general list:
1. eval is not safe, how to make the browser side safely execute js code?
2. If you encounter an interface that requires file upload, what do you need to do?
3. Since the test can be automated on the front end, can I put these test cases on the server and poll automatically? The
above is the entire content of this article, hope It is helpful to everyone's study. Friends who have been helped are welcome to like and comment.

Guess you like

Origin blog.csdn.net/Chaqian/article/details/106532812