从事测试工作以来,接口测试接触的比较多,主要因为工作的需要,部门项目产品线比较多(共6条),且项目采用微服务架构,因此公共服务的回归在日常测试工作中比较重要,可以防止新功能影响旧功能或是其他产品线修改公共服务影响到本产品。但是接口测试的编写还是比较麻烦和费时的,平时很难抽出时间去写,于是没事就在捉摸有什么方法可以提高效率呢?俗话说,孰能生巧,当然熟悉才能发现规律,当我们写的接口比较多时,我们就会发现其实接口测试中有好多是相同类似的结构,有一定的规则。那么我们就会想能不能通过某种方式将相同的部分自动化掉呢,比如在某路径下创建类、在类中导入包、自动创建函数等等。然后我们就是去准备数据和添加验证点的工作呢?这样写接口测试的工作效率不就提高了吗?然后我们就有时间去喝着咖啡,聊聊天,这样的日子是不是很美好呢,令人向往吧?心动不如行动,说干咱就干。测试工作中主要负责测试web端,对于模板文件有一定的了解,知道基于FreeMarker和后端返回的Vo可以自动生成网页展示给用户,于是想是不是可以基于规则实现接口测试代码生成的自动化呢?经过调研及尝试,功夫不负有心人,最终实现了接口测试代码生成的自动化,并应用于日常的工作中,提高了写接口测试的效率,现在分享出来,希望可以帮助有缘人哈。
简单说就是: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;
}
}
效果图如下:
四、背后的思考
通过上面两个简单的示例我们了解到所谓的自动生成代码其实就是:
1、定义java类模板文件 2、定义模板数据 3、引用模板文件(.ftl)与模板数据合并生成Java类。
整体思路比较简单,关键是用到对的地方,提高工作效率。通过这种方式,我在C/S客户端上填写接口url、请求方式、参数及用例标题等信息 然后一键生成,想想那是多么爽、多么痛快的一件事(当然 前提是你的模板类要编写的非常强大、通用),而你也许还在不停的 Ctrl+C、Ctrl+V和Ctrl+F呢。
本篇文章目前只支持了get、post和payload三种传参方式,put和delete也可以扩展,有兴趣的同学可以参考代码自动扩展。由于项目测试框架已经成熟,在此基础上又有很多用例,因此采用模板技术,灵活且解耦,可以方便实现接口测试代码生成的自动化,提高接口测试代码编写的效率。