Teach you to build a visual interface automated test system

Nowadays, interface development has almost become the standard of an Internet company. Whether it is a web or an app, even a small program, it is inseparable from the interface as a support. Of course, the range of interfaces 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 amount of interface data, if they are better managed and tested, they will be a headache. The most important thing is that many business scenarios require multiple The interface is jointly debugged. Therefore, after the interface development is completed, a round of automated testing can quickly feedback the current system status. Faced with such a demand, a tester-friendly visual interface automated testing system is essential. So, let's talk to you today about how to implement a small http interface automated testing system!

We take DOClever as a model for this system to illustrate, because it is open source, and 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, from UI test cases that do not require a single line of code. Writing, to more powerful and flexible code patterns, all provide very friendly support.

system requirement:

1. In a test case, you can freely edit the input parameters of an interface, run and judge whether the parameters are correct, and at the same time you can view the complete input and output data of the interface

2. A set of interfaces can be tested in a test case, their execution order can be adjusted freely, and the input parameters of the next interface can be used according to the output parameters of the previous interface.

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

4. Provide a set of auxiliary tools, which can quickly realize 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 its test case, get the return value to realize the linkage on the data

6. When the user inputs, it can realize quick prompts 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 side adopts the express framework. Of course, you can also choose koa or other frameworks according to your preferences

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

Architecture Design:

 

First give a dynamic diagram of an automated test:

Then, let's start with the most basic proxy server if the interface data is forwarded.

The so-called interface data forwarding is nothing more than using node as a layer of proxy transfer. Fortunately, node is actually very good at doing this kind of work. We regard each interface request as a post request to the proxy server, and 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. Then we use the stream of the node to pipe directly to the real request, and return after receiving the real interface. After the data, the data will be piped to the response of the original post request, thus completing a proxy forwarding.

A few things to note are:

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, filter out the host, origin and other information, and retain the custom headers and cookies that the customer needs to request.

3. In many cases, the interface may return a jump, then we need to process this jump, request the jump address again and accept the return data.

4. We need to filter the data returned by the interface one by one, focusing on cookies. We need to process the set-cookie field and remove the unwritable part of the browser, so as to ensure that when we call the login interface, we can write locally. Enter the correct cookie and 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);
    });
};

Take a screenshot of the data sent to the proxy server for a post request:

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

Then, let's go up the request distribution, let's take a look at the top layer of the entire automated test, that is, the construction of the h5 visual interface (the core part will be left at the end).

Here's a picture for you first:

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

1. Each button in the above figure can generate a test node. For example, if 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 identifier of the node. We can drag and drop the node to change the position of the node, but the ID remains unchanged.

When we click the run button, the system will generate pseudocode based on the current node order.

The pseudocode generated above is

var $0=await get training list data ({param:{},query:{},header:{},body:{},});

log("print log:");

var $2=await every day (...[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 color is the other embedded use cases. We can see the operation of the interface and we can pass in our custom input parameters, the meaning of param, query, header and body I believe everyone can understand, and we use a syntax parameter expander of es6 to implement the parameter transfer of the use case, so that an array can be expanded into a parameter. Here are a few points to explain:

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

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

3. Regarding the association of 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, such as $0.data.username represents the value of the username field in the data returned by the interface to get the training list data.

OK, let's go back to our previous topic, how to generate these test nodes on the visual interface, such as what happens when we click a button.

 

1. First, we click the interface button, and a selection box will pop up for us to select the interface information. You can customize the interface data collection here, just 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 passes the test, we will use the return value of the test case here, as shown in the following figure:

Undecided means that the execution result of the current use case is unknown. Passed means the use case passed, and failed means the use case failed. 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 complete data structure information of all nodes, please refer to the source code in GitHub and OSChina

Ok, let's go on and say, when we click the run button, the test node will be converted into pseudo code, which is easier to understand. For example, the interface node will be converted into

var $0=await get 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;
}

It can be seen that 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.

 

Well, 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's first use a table to represent this process

Let's look at it step by step:

1. Parse 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 take a 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 see 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;
});

OK, in general our visual interface automation testing platform is complete, but there are a lot of details involved, I will list them roughly:

1. eval is insecure, how to make the browser side execute js code safely?

2. What should I do if I encounter an interface that requires file uploading?

3. Since the test can be automated on the front end, can I put these test cases on the server and poll them automatically?

 

For the complete code, you can refer to GitHub and OSChina . You are also welcome to support DOClever . We will get better and better at the interface. Now we have launched the desktop side to provide more powerful functions and a better user experience. Interested friends can add me qq: 395414574 to discuss progress together~

 

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324961594&siteId=291194637