一种基于FreeMarker的接口测试代码自动生成方案

阿凡达2018-08-10 09:24

 从事测试工作以来,接口测试接触的比较多,主要因为工作的需要,部门项目产品线比较多(共6条),且项目采用微服务架构,因此公共服务的回归在日常测试工作中比较重要,可以防止新功能影响旧功能或是其他产品线修改公共服务影响到本产品。但是接口测试的编写还是比较麻烦和费时的,平时很难抽出时间去写,于是没事就在捉摸有什么方法可以提高效率呢?俗话说,孰能生巧,当然熟悉才能发现规律,当我们写的接口比较多时,我们就会发现其实接口测试中有好多是相同类似的结构,有一定的规则。那么我们就会想能不能通过某种方式将相同的部分自动化掉呢,比如在某路径下创建类、在类中导入包、自动创建函数等等。然后我们就是去准备数据和添加验证点的工作呢?这样写接口测试的工作效率不就提高了吗?然后我们就有时间去喝着咖啡,聊聊天,这样的日子是不是很美好呢,令人向往吧?心动不如行动,说干咱就干。测试工作中主要负责测试web端,对于模板文件有一定的了解,知道基于FreeMarker和后端返回的Vo可以自动生成网页展示给用户,于是想是不是可以基于规则实现接口测试代码生成的自动化呢?经过调研及尝试,功夫不负有心人,最终实现了接口测试代码生成的自动化,并应用于日常的工作中,提高了写接口测试的效率,现在分享出来,希望可以帮助有缘人哈。

一、架构分析
   在项目组中,我们的接口测试框架是基于TestNG框架,因此存在一定规律。我们项目目前分为以下几部分:
1.类前面的包名、导入的第三方包及作者和编码时间
package com.netease.qa.study.testcase.p.signup.batch;
import javax.annotation.Resource;
import org.apache.http.protocol.HTTP;
import org.apache.log4j.Logger;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.netease.qa.auth.WebAuth;
import com.netease.qa.base.BaseWEBTest;
import com.netease.qa.meta.HttpType;
import com.netease.qa.meta.Response;
import net.sf.json.JSONObject;
/**
 * 
 * @author hzxxxxxxx   2017/11/23
 *
 */
2.类主体
public class ValidateCourseAuthWhenAssignAll Test extends BaseWEBTest 
{
    //声明日志对象
    s tatic Logger logger =Logger.getLogger( ValidateCourseAuthWhenAssignAllTest .class.getName());
    //测试接口url
    static String  validateCourseAuthWhenAssignAllUrl="http://fes2.study.163.com/p/signup/batch/validateCourseAuthWhenAssignAll.json";
   //获取用户 
    @Resource
    WebAuth webAuthBeanfes;
    //构造数据,调用httpclient发起请求
    static public Response validateCourseAuthWhenAssignAllTest (WebAuth client, String ids)
    {
J SONObject jsonObject = new JSONObject();
      jsonObject.accumulate("ids",ids);
  Response response = request.http(client, HttpType.POST_PAYLOAD,  validateCourseAuthWhenAssignAllUrl, jsonObject.toString(), HTTP.UTF_8);
  return response;
    }
//验证部分
    @Test(testName = "",dataProvider = " validateCourseAuthWhenAssignAllTestData" ,groups = {"critical","major"})
    public void validateCourseAuthWhenAssignAllTest(String caseInfo,String ids,String expectCode,String expectMessage,String expectcheckResult,String exceptionTerms
)
    {
        l ogger.info("开始执行 com.netease.qa.study.testcase. p.signup.batch中的ValidateCourseAuthWhenAssignAllTest方法");
        Response response =  validateCourseAuthWhenAssignAllTest( webAuthBeanfes, ids);
S tring body = response.getBody();
logger.info("body:"+body);
Assert.assertEquals(response.getCode(),200, "http状态码不正确");
JSONObject json = JSONObject.fromObject(body);
Assert.assertEquals(json.getString("code"), expectCode, "code码不正确");
Assert.assertEquals(json.getString("message"), expectMessage, "message码不正确");
logger.info("结束执行 com.netease.qa.study.testcase .p.signup.batch中的ValidateCourseAuthWhenAssignAllTest方法");
    }
//数据准备部分
    @DataProvider
public Object[][] validateCourseAuthWhenAssignAllTestData(){
return new Object[][] {   
{"case1-",“”“”},
{"case2-",“”“”},
};
}
分析框架及日久生情,很容易便可以发现写接口测试的规则及其中的烦恼,规则是大部分内容是可以固定不变的,需要变得是url、参数、数据准备和验证点,因此我们可以将不变的内容或有规则的内容(参数化)通过ftl来实现。通过分析可以红色部分是固定不变的内容而蓝色部分是可以参数化的,因此基于次规则,我们可以基于ftl实现代码生成的自动化。 可以解决原来复制黏贴带来的枯燥无味且替换不全的问题
目前接口测试传参的方式常用的有三种:get、pot及payload。不同的传参方式对应调用 httpclient请求 的请求题是不一样的,但是我们会发现他们各自是有一定的规律,
首先我们看下各自对应的 httpclient请求:
Get请求:  
POST请求:  
PayLoad请求:
因此我们在编写自动化脚本时可以将传参方式作为参数,然后根据参数智能化定制httpclient请求体。
 由分析可知,我们了解了接口测试代码的规则,那么我们接下来就是需要将静态部分利用FreeMarker模板配置,动态的数据我们利用java的实体返回给模板,这样就实现了接口测试代码生成的自动化。
二、FreeMarker的简单介绍
   在进入正文前,让我们首先简单、快速了解一下FreeMarker。
  (做过Web开发的朋友肯定都是相当熟悉的)
1、概述:FreeMarker是一款模板引擎:即一种基于模板、用来生成输出文本的通用工具。更多的是被用来设计生成HTML页面。

简单说就是:FreeMarker是使用模板生成文本页面来呈现已经准备好的数据。如下图表述

package com.netease.qa.autoCode;

import java.util.List;

/**

 * 属性类

 * @author  hzfuershuai

 *

 */

public class Test {

// 实体所在的包名

private String javaPackage;

// 实体类名

private String className;

// 方法名

private String methodName;

// 父类名

private String superclass;

//接口传参

private String papms;

//接口传参

private String papms2;

//接口url

private String url;

//接口方式

private String way;

//get方法拼接url

private String getMethodPamas;

// 参数集合

private List<String> properties;

// 用例标题集合

private List<String> caselist;

// 是否有构造函数

private boolean constructors;

private String date;

public String getJavaPackage() {

return javaPackage;

}

public void setJavaPackage(String javaPackage) {

this.javaPackage = javaPackage;

}

public String getClassName() {

return className;

}

public void setClassName(String className) {

this.className = className;

}

public String getMethodName() {

return methodName;

}

public void setMethodName(String methodName) {

this.methodName = methodName;

}

public String getSuperclass() {

return superclass;

}

public void setSuperclass(String superclass) {

this.superclass = superclass;

}

public String getPapms() {

return papms;

}

public void setPapms(String papms) {

this.papms = papms;

}

public String getPapms2() {

return papms2;

}

public void setPapms2(String papms2) {

this.papms2 = papms2;

}

public String getUrl() {

return url;

}

public void setUrl(String url) {

this.url = url;

}

public String getWay() {

return way;

}

public void setWay(String way) {

this.way = way;

}

public String getDate() {

return date;

}

public void setDate(String date) {

this.date = date;

}

public String getGetMethodPamas() {

return getMethodPamas;

}

public void setGetMethodPamas(String getMethodPamas) {

this.getMethodPamas = getMethodPamas;

}

public List<String> getProperties() {

return properties;

}

public void setProperties(List<String> properties) {

this.properties = properties;

}

public List<String> getCaselist() {

return caselist;

}

public void setCaselist(List<String> caselist) {

this.caselist = caselist;

}

public boolean isConstructors() {

return constructors;

}

public void setConstructors(boolean constructors) {

this.constructors = constructors;

}

}  


2、在项目根目录下新建"template"文件夹,用来存放我们的Template file, 新建实体模板testmark.ftl作为我们的模板文件,我们根据我们接口测试代码规则来定制模板,红色的我们在模板中静态写死,蓝色部分我们可以通过参数的形式传到ftl文件,具体内容如下:

package ${entity.javaPackage};

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.log4j.Logger;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import com.netease.qa.auth.WebAuth;
import com.netease.qa.base.BaseWEBTest;
import com.netease.qa.meta.HttpType;
import com.netease.qa.meta.Response;

import net.sf.json.JSONObject;
/**
*
* @author hzfuershuai ${entity.date}
*
*/
public class {entity.className}Test<#if entity.superclass?has_content> extends{entity.superclass} </#if>
{

static Logger logger =Logger.getLogger(${entity.className}Test.class.getName());
static String entity.methodNameUrl="{entity.url}";
<#if entity.way =="get">
static String ${entity.methodName}BaseUrl = "";
</#if>
@Resource
WebAuth webAuthBeanfes;
static public Response entity.methodNameTest(WebAuthclient,{entity.papms})
{
<#if entity.way =="post">
List<NameValuePair> nvp = new ArrayList<NameValuePair>();
<#list entity.properties as property>
nvp.add(new BasicNameValuePair("property",{property}));
</#list>
Response response = request.http(client, HttpType.POST, ${entity.methodName}Url, nvp, HTTP.UTF_8);
<#elseif entity.way =="payload">
JSONObject jsonObject = new JSONObject();
<#list entity.properties as property>
jsonObject.accumulate("property",{property});
</#list>
Response response = request.http(client, HttpType.POST_PAYLOAD, ${entity.methodName}Url, jsonObject.toString(), HTTP.UTF_8);
<#elseif entity.way =="get">
List<NameValuePair> nvp = new ArrayList<NameValuePair>();
entity.methodNameBaseUrl={entity.methodName}Url+${entity.getMethodPamas};
Response response = request.http(client, HttpType.GET, ${entity.methodName}BaseUrl,nvp, HTTP.UTF_8);
</#if>
return response;
}
@Test(testName = "",dataProvider = "${entity.methodName}TestData",groups = {"critical","major"})
public void entity.methodNameTest(StringcaseInfo,{entity.papms},String expectCode,String expectMessage)
{

3、自动生成实体类,通过运行此类可以传入接口url、请求类型等方式、用例标题等内容,该类会根据请求类型自动解析参数及url,实现请求类型的定制化,目前只支持主流的get、post和payload三种形式,模板的灵活性与代码框架解耦,因此当我们增加类型时只需要在脚本中做相应的扩展即可,非常的灵活方便,具体内容如下: TestGeneratorClient.java  

package com.netease.qa.autoCode;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import net.sf.json.JSONObject;
/**
* 自动生成实体类客户端
* @author hzfuershuai
*
*/
public class TestGeneratorClient {

private static File javaFile = null;
private static String path="";
private static String[] pathstr=new String[5];
private static String[] pathstr1=new String[5];
public static void main(String[] args) {
Configuration cfg = new Configuration();
String packageName="";
String className="";
String methodName="";
String url="";
String papms="";
String papms2="";
String str="";
String getMethodPamas="";
Scanner scan = new Scanner(System.in);
//http://fes2.study.163.com/j/project/createProject.do
System.out.println("请输入接口测试的url:(example:http://fes2.study.163.com/j/project/createProject.do)");
url=scan.next();

String[] u=url.split("http://.*?.163.com");
String[] u2=u[1].split("/");
for (int i=0;i<u2.length-1;i++)
packageName+=u2[i]+".";
packageName=packageName.substring(0, packageName.length()-1);
System.out.println(packageName);
String[] u3=u2[u2.length-1].split("\\.");
methodName=u3[0];
className=methodName.substring(0,1).toUpperCase() + methodName.substring(1);
System.out.println(methodName);
System.out.println(className);
Date now = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
String date = dateFormat.format( now );
System.out.println("请输入接口传参方式:(post,get,payLoad)");
String way=scan.next();

if(way.equalsIgnoreCase("post"))
{
System.out.println("请输入接口所需参数:");
str=scan.next();
String s[]=str.split("=.*?&");
for(int i=0;i<s.length;i++)
{
//System.out.println(s[i]);
papms=papms+"String "+s[i]+",";
papms2=papms2+s[i]+",";


}
//System.out.println(papms);
//System.out.println(papms2);
papms=papms.substring(0, papms.indexOf("="));
papms2=papms2.substring(0, papms2.indexOf("="));
}

else if(way.equalsIgnoreCase("payLoad"))
{
System.out.println("请输入接口所需参数:");

str=scan.next();
//只取参数
JSONObject json_test = JSONObject.fromObject(str);
Iterator it = json_test.keys();
while (it.hasNext()) {
String key = it.next().toString();
System.out.println(key);
papms=papms+"String "+key+",";
papms2=papms2+key+",";
}
papms=papms.substring(0, papms.length()-1);
papms2=papms2.substring(0, papms2.length()-1);

}
else if(way.equalsIgnoreCase("get"))
{
System.out.println("请输入接口所需参数:");
str=scan.next();
String s[]=str.split("=.*?&");
for(int i=0;i<s.length;i++)
{
//System.out.println(s[i]);
papms=papms+"String "+s[i]+",";
papms2=papms2+s[i]+",";
}

for(int i=1;i<s.length-1;i++)
{
getMethodPamas=getMethodPamas+"\""+"&"+s[i]+"="+"\""+"+"+s[i]+"+";
}
getMethodPamas="\""+"?"+s[0]+"="+"\""+"+"+s[0]+"+"+getMethodPamas+"\""+"&"+s[s.length-1].substring(0, s[s.length-1].indexOf("="))+"="+"\""+"+"+s[s.length-1].substring(0, s[s.length-1].indexOf("="));
//System.out.println(getMethodPamas);
//System.out.println(papms);
//System.out.println(papms2);
papms=papms.substring(0, papms.indexOf("="));
papms2=papms2.substring(0, papms2.indexOf("="));
}

System.out.println(papms);
System.out.println(papms2);

String[] buffer=papms2.split(",");
List<String>plist=new ArrayList<String>();
for(int i=0;i<buffer.length;i++)
{
//System.out.println(buffer[i]);
plist.add(buffer[i]);

}
List<String>caselist=new ArrayList<String>();
System.out.println("请输入接口所需用例标题:");
while(!scan.hasNext("Q"))
{
str=scan.next();
caselist.add(str);
str="";
}
path=EntityGeneratorClient.class.getClassLoader().getResource("").toString();
pathstr=path.split("/target/classes/");
pathstr1=pathstr[0].split("file:/");
path=pathstr1[1];
System.out.println(path+"/src/main/java/com/netease/qa/autoCode");

System.out.println(path+"/src/main/java/com/netease/qa/autoCode");
try {
// 步骤一:指定 模板文件从何处加载的数据源,这里设置一个文件目录
cfg.setDirectoryForTemplateLoading(new File((path+"/src/main/java/com/netease/qa/autoCode")));
cfg.setObjectWrapper(new DefaultObjectWrapper());

// 步骤二:获取 模板文件
Template template = cfg.getTemplate("testmark.ftl");

// 步骤三:创建 数据模型
Map<String, Object> root = createDataModel(packageName,className,methodName,papms,plist,papms2,url,way.toLowerCase(),getMethodPamas,caselist,date);

// 步骤四:合并 模板 和 数据模型
// 创建.java类文件
if(javaFile != null){
//若文件已存在,抛出异常
if(javaFile.exists()){
throw new Exception("The new file already exists!");
}
else
{
Writer javaWriter = new FileWriter(javaFile);
template.process(root, javaWriter);
javaWriter.flush();
System.out.println("文件生成路径:" + javaFile.getCanonicalPath());

javaWriter.close();
}

}
// 输出到Console控制台
Writer out = new OutputStreamWriter(System.out);
template.process(root, out);
out.flush();
out.close();

} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

/**
* 创建数据模型
* @return
*/
private static Map<String, Object> createDataModel(String packageName,String className,String methodName,String papms,List<String> properties,String papms2,String url,String way,String getMethodPamas,List<String>caselist,String date) {
Map<String, Object> root = new HashMap<String, Object>();
String rootpath=path+"/src/test/java";
Test user = new Test();
user.setJavaPackage("com.netease.qa.study.testcase"+packageName); // 创建包名
user.setClassName(className); // 创建类名
user.setMethodName(methodName);
user.setConstructors(false); // 是否创建构造函数
user.setSuperclass("BaseWEBTest");//父类
user.setPapms(papms);//构建参数
user.setPapms2(papms2);//构建参数去除String
user.setProperties(properties);
user.setUrl(url);
user.setWay(way);
user.setGetMethodPamas(getMethodPamas);
user.setCaselist(caselist);
user.setDate(date);
// 创建.java类文件

File outDirFile = new File(rootpath);
/*if(!outDirFile.exists()){
outDirFile.mkdir();
}*/

javaFile = toJavaFilename(outDirFile, user.getJavaPackage(), user.getClassName());

root.put("entity", user);
return root;
}


/**
* 创建.java文件所在路径 和 返回.java文件File对象
* @param outDirFile 生成文件路径
* @param javaPackage java包名
* @param javaClassName java类名
* @return
*/
private static File toJavaFilename(File outDirFile, String javaPackage, String javaClassName) {
String packageSubPath = javaPackage.replace('.', '/');
File packagePath = new File(outDirFile, packageSubPath);
File file = new File(packagePath, javaClassName+"Test" + ".java");
if(!packagePath.exists()){
packagePath.mkdirs();
}
return file;
}

}


4、运行程序 ,根据参数我们就可以运行步骤5中的方法实现接口测试代码的自动化。目前接口代码自动化生成需要输入一定的参数,比如接口的url,传参类型、参数、及数据准备的用例等,步骤5的代码可以根据输入的参数自动解析,根据传参方式的不同,自动生成对应的接口测试代码,实现了接口测试代码生成的自动化和智能化,提高了日常接口测试代码编写的效率。
下面会介绍该工具的使用:
第一步:运行步骤5中的程序,然后根据提示输入相应的参数,截图如下:
根据这样的输入,我们会在对应的路径下生成代码:
我们将会在项目根目录下生成文件夹以及自动生成的实体类

效果图如下:
                                            


四、背后的思考

    通过上面两个简单的示例我们了解到所谓的自动生成代码其实就是:

    1、定义java类模板文件 2、定义模板数据  3、引用模板文件(.ftl)与模板数据合并生成Java类

    整体思路比较简单,关键是用到对的地方,提高工作效率。通过这种方式,我在C/S客户端上填写接口url、请求方式、参数及用例标题等信息 然后一键生成,想想那是多么爽、多么痛快的一件事(当然 前提是你的模板类要编写的非常强大、通用),而你也许还在不停的 Ctrl+C、Ctrl+V和Ctrl+F呢。

本篇文章目前只支持了get、post和payload三种传参方式,put和delete也可以扩展,有兴趣的同学可以参考代码自动扩展。由于项目测试框架已经成熟,在此基础上又有很多用例,因此采用模板技术,灵活且解耦,可以方便实现接口测试代码生成的自动化,提高接口测试代码编写的效率。


网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者付二帅授权发布。