接口自动化框架

项目说明

  • 本框架是一套基于maven+java+TestNG+httpclient+poi+jsonpath+ExtentReport而设计的数据驱动接口自动化测试框架,TestNG 作为执行器,poi用于读取存放于excel的接口用例,jsonPath用于校验返回值,以及提取返回值。本框架无需你使用代码编写用例,在excel中即可进行接口用例编写,接口依赖关联,接口断言,控制用例的运行。

技术栈

  • maven
  • java
  • TestNG
  • httpclient
  • poi
  • jsonpath
  • ExtentReport

环境部署

  • 安装jdk8,并配置好环境变量
  • maven中直接导入项目工程包,导入成功后,maven会自动下载当前项目的所有依赖包

代码设计与功能说明

1、定义运行配置文件 api-config.xml

api请求根路径、请求头及初始化参数值可以在api-config上进行配置。

  • rootUrl: 必须的配置,api的根路径,在调用api时用于拼接,配置后,会在自动添加到用例中的url的前缀中。
  • headers: 非必须配置,配置后在调用api时会将对应的name:value值设置到所有请求的请求头中header-name:header-value。
  • params:非必须配置,公共参数,通常放置初始化配置数据,所有用例执行前,会将params下所有的param配置进行读取并存储到公共参数池中,在用例执行时,使用特定的关键字(${param_name})可以获取。具体如下:

api-config.xml配置信息

<?xml version="1.0" encoding="UTF-8"?>
<root>
<rootUrl>http://127.0.0.1:12306</rootUrl>
<headers>
<!-- 配置为自己的参数 -->
<header name="Content-Type" value="application/json;charset=UTF-8"></header>
</headers>
<params>
<param name="" value=""></param>
</params>
<project_name>接口自动化测试报告demo</project_name>
</root>

2、测试用例的设计

测试用例以excel格式的文件保存,除表头外,一行代表一个api用例。执行时会依次从左到右,从上到下执行。case/api-data.xls测试用例的数据格式如下:

  • run:标记为‘Y’时,该行数据会被读取执行;标记为‘N’则不被执行
  • description:该用例描述,在报告中体现。
  • method:该api测试用例的请求方法。
  • url:该api测试用例的请求路径。
  • 说明:
  • param:请求方法为post时,body的内容(暂只支持json,不支持xml)
  • verify:对于api请求response数据的验证(可使用jsonPath进行校验)。校验多个使用“;”进行隔开。
  • 若verify填写值为:$.username=wuya;$.userID=22 ,则会校验返回值中$.username的值为wuya,$.userID的值为22,只要有一个校验错误,后面的其他校验项将停止校验。
  • save:使用jsonPath对response的数据进行提取存储。
  • 说明:若save值为:id=$.userId;age=$.age ,接口实际返回内容为:{"username":"chenwx","userId":"1000","age":"18"},则接口执行完成后,会将公共参数userId的值存储为1000,age存储为18。公共参数可在后面的用例中进行使用。
  • 公共关联池中的公共参数使用
  • 测试用例excel表中可以使用‘${param_name}’占位符,在执行过程中如果判断含有占位符,则会将该值替换为公共参数里面的值,如果找不到将会报错。具体使用格式如下:

{
"token":"${g_token}",
"vpl":"AJ3585"
}

3、函数助手

测试用例excel表中可以使用‘__funcName(args)’占位符,在执行过程中如果判断含有该占位符,且funcName存在,则会执行相应的函数后进行替换。部分函数说明如下:

  • __random(param1,param2):随机生成一个定长的字符串(不含中文)。param1:长度(非必填,默认为6),param2:纯数字标识(非必填,默认为false)。
  • __randomText(param1): 随机生成一个定长的字符串(含中文)。param1:长度(非必填,默认为6)
  • __date(param1): 生成执行该函数时的时间格式化字符串。param1为转换的格式,默认为生成当前13位时间戳。
  • 具体使用格式如下:

{
"drivers":"张三",
"cmsuer":"__random(8,false)",
"time":"__date()"
}

函数random执行时会产生8位长度的随机字符串,并传给变量cmsuer;函数date在执行时,会产生一个13位的时间戳,并传给变量time。

4、测试执行主程序


package test.com.sen.api;

import com.alibaba.fastjson.JSON;
import com.sen.api.beans.ApiDataBean;
import com.sen.api.configs.ApiConfig;
import com.sen.api.excepions.ErrorRespStatusException;
import com.sen.api.listeners.AutoTestListener;
import com.sen.api.listeners.RetryListener;
import com.sen.api.utils.*;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.util.EntityUtils;
import org.dom4j.DocumentException;
import org.testng.Assert;
import org.testng.ITestContext;
import org.testng.annotations.*;
import org.testng.annotations.Optional; import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher; @Listeners({ AutoTestListener.class, RetryListener.class })
public class ApiTest extends TestBase { /**
* api请求跟路径
*/
private static String rootUrl; /**
* 跟路径是否以‘/’结尾
*/
private static boolean rooUrlEndWithSlash = false; /**
* 所有公共header,会在发送请求的时候添加到http header上
*/
private static Header[] publicHeaders; /**
* 是否使用form-data传参 会在post与put方法封装请求参数用到
*/
private static boolean requestByFormData = false; /**
* 配置
*/
private static ApiConfig apiConfig; /**
* 所有api测试用例数据
*/
protected List<ApiDataBean> dataList = new ArrayList<ApiDataBean>(); private static HttpClient client; /**
* 初始化测试数据
*
* @throws Exception
*/
@Parameters("envName")
@BeforeSuite
public void init(@Optional("api-config.xml") String envName) throws Exception {
String configFilePath = Paths.get(System.getProperty("user.dir"), envName).toString();
ReportUtil.log("api config path:" + configFilePath);
apiConfig = new ApiConfig(configFilePath);
// 获取基础数据
rootUrl = apiConfig.getRootUrl();
rooUrlEndWithSlash = rootUrl.endsWith("/"); Map<String, String> params = apiConfig.getParams();
setSaveDates(params); List<Header> headers = new ArrayList<Header>();
apiConfig.getHeaders().forEach((key, value) -> {
Header header = new BasicHeader(key, value);
if(!requestByFormData && key.equalsIgnoreCase("content-type") && value.toLowerCase().contains("form-data")){
requestByFormData=true;
}
headers.add(header);
});
publicHeaders = headers.toArray(new Header[headers.size()]);
client = new SSLClient();
client.getParams().setParameter(
CoreConnectionPNames.CONNECTION_TIMEOUT, 60000); // 请求超时
client.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 60000); // 读取超时
} @Parameters({ "excelPath", "sheetName" })
@BeforeTest
public void readData(@Optional("case/api-data.xls") String excelPath, @Optional("Sheet1") String sheetName) throws DocumentException {
dataList = readExcelData(ApiDataBean.class, excelPath.split(";"),
sheetName.split(";"));
} /**
* 过滤数据,run标记为Y的执行。
*
* @return
* @throws DocumentException
*/
@DataProvider(name = "apiDatas")
public Iterator<Object[]> getApiData(ITestContext context)
throws DocumentException {
List<Object[]> dataProvider = new ArrayList<Object[]>();
for (ApiDataBean data : dataList) {
// poi解析处理Excel表时,若单元格中的布尔值为Y或者true,则解析到的布尔值为true;若单元格中的布尔值为false或者其他值或者为空,则解析到的布尔值为false
if (data.isRun()) {
dataProvider.add(new Object[] { data });
}
}
return dataProvider.iterator();
} @Test(dataProvider = "apiDatas")
public void apiTest(ApiDataBean apiDataBean) throws Exception {
ReportUtil.log("--- test start ---");
if (apiDataBean.getSleep() > 0) {
// sleep休眠时间大于0的情况下进行暂停休眠
ReportUtil.log(String.format("sleep %s seconds",
apiDataBean.getSleep()));
Thread.sleep(apiDataBean.getSleep() * 1000);
}
String apiParam = buildRequestParam(apiDataBean);
// 封装请求方法
HttpUriRequest method = parseHttpRequest(apiDataBean.getUrl(),
apiDataBean.getMethod(), apiParam);
String responseData;
try {
// 执行
HttpResponse response = client.execute(method);
int responseStatus = response.getStatusLine().getStatusCode();
ReportUtil.log("返回状态码:"+responseStatus);
if (apiDataBean.getStatus()!= 0) {
Assert.assertEquals(responseStatus, apiDataBean.getStatus(),
"返回状态码与预期不符合!");
} HttpEntity respEntity = response.getEntity();
Header respContentType = response.getFirstHeader("Content-Type");
if (respContentType != null && respContentType.getValue() != null
&& (respContentType.getValue().contains("download") || respContentType.getValue().contains("octet-stream"))) {
String conDisposition = response.getFirstHeader(
"Content-disposition").getValue();
String fileType = conDisposition.substring(
conDisposition.lastIndexOf("."),
conDisposition.length());
String filePath = "download/" + RandomUtil.getRandom(8, false)
+ fileType;
InputStream is = response.getEntity().getContent();
Assert.assertTrue(FileUtil.writeFile(is, filePath), "下载文件失败。");
// 将下载文件的路径放到{"filePath":"xxxxx"}进行返回
responseData = "{\"filePath\":\"" + filePath + "\"}";
} else {
responseData=EntityUtils.toString(respEntity, "UTF-8");
}
} catch (Exception e) {
throw e;
} finally {
method.abort();
}
// 输出返回数据log
ReportUtil.log("resp:" + responseData);
// 验证预期信息
verifyResult(responseData, apiDataBean.getVerify(),
apiDataBean.isContains()); // 对返回结果进行提取保存。
saveResult(responseData, apiDataBean.getSave());
} private String buildRequestParam(ApiDataBean apiDataBean) {
// 分析处理预参数 (函数生成的参数)
String preParam = buildParam(apiDataBean.getPreParam());
savePreParam(preParam);// 保存预存参数 用于后面接口参数中使用和接口返回验证中
// 处理参数
String apiParam = buildParam(apiDataBean.getParam());
return apiParam;
} /**
* 封装请求方法
*
* @param url
* 请求路径
* @param method
* 请求方法
* @param param
* 请求参数
* @return 请求方法
* @throws UnsupportedEncodingException
*/
private HttpUriRequest parseHttpRequest(String url, String method, String param) throws UnsupportedEncodingException {
// 处理url
url = parseUrl(url);
ReportUtil.log("method:" + method);
ReportUtil.log("url:" + url);
ReportUtil.log("param:" + param.replace("\r\n", "").replace("\n", ""));
//upload表示上传,也是使用post进行请求
if ("post".equalsIgnoreCase(method) || "upload".equalsIgnoreCase(method)) {
// 封装post方法
HttpPost postMethod = new HttpPost(url);
postMethod.setHeaders(publicHeaders);
//如果请求头的content-type的值包含form-data 或者 请求方法为upload(上传)时采用MultipartEntity形式
HttpEntity entity = parseEntity(param,requestByFormData || "upload".equalsIgnoreCase(method));
postMethod.setEntity(entity);
return postMethod;
} else if ("put".equalsIgnoreCase(method)) {
// 封装put方法
HttpPut putMethod = new HttpPut(url);
putMethod.setHeaders(publicHeaders);
HttpEntity entity = parseEntity(param,requestByFormData );
putMethod.setEntity(entity);
return putMethod;
} else if ("delete".equalsIgnoreCase(method)) {
// 封装delete方法
HttpDelete deleteMethod = new HttpDelete(url);
deleteMethod.setHeaders(publicHeaders);
return deleteMethod;
} else {
// 封装get方法
HttpGet getMethod = new HttpGet(url);
getMethod.setHeaders(publicHeaders);
return getMethod;
}
} /**
* 格式化url,替换路径参数等。
*
* @param shortUrl
* @return
*/
private String parseUrl(String shortUrl) {
// 替换url中的参数
shortUrl = getCommonParam(shortUrl);
if (shortUrl.startsWith("http")) {
return shortUrl;
}
if (rooUrlEndWithSlash == shortUrl.startsWith("/")) {
if (rooUrlEndWithSlash) {
shortUrl = shortUrl.replaceFirst("/", "");
} else {
shortUrl = "/" + shortUrl;
}
}
return rootUrl + shortUrl;
} /**
* 格式化参数,如果是from-data格式则将参数封装到MultipartEntity否则封装到StringEntity
* @param param 参数
* @param formData 是否使用form-data格式
* @return
* @throws UnsupportedEncodingException
*/
private HttpEntity parseEntity(String param,boolean formData) throws UnsupportedEncodingException{
if(formData){
Map<String, String> paramMap = JSON.parseObject(param,
HashMap.class);
MultipartEntity multiEntity = new MultipartEntity();
for (String key : paramMap.keySet()) {
String value = paramMap.get(key);
Matcher m = funPattern.matcher(value);
if (m.matches() && m.group(1).equals("bodyfile")) {
value = m.group(2);
multiEntity.addPart(key, new FileBody(new File(value)));
} else {
multiEntity.addPart(key, new StringBody(paramMap.get(key)));
}
}
return multiEntity;
}else{
return new StringEntity(param, "UTF-8");
}
} }

5、测试总执行器testng.xml(收集测试用例,批量执行并生成测试报告)


<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="接口自动化测试" verbose="1" preserve-order="true" parallel="false">
<test name="自动化测试用例">
<parameter name="excelPath" value="case/api-data.xls"></parameter>
<parameter name="sheetName" value="Sheet1"></parameter>
<classes>
<class name="test.com.sen.api.ApiTest">
<methods>
<include name="apiTest"></include>
</methods>
</class>
</classes>
</test>
<listeners>
<listener class-name="com.sen.api.listeners.AutoTestListener"></listener>
<listener class-name="com.sen.api.listeners.RetryListener"></listener>
<!-- ExtentReport 报告 -->
<listener class-name="com.sen.api.listeners.ExtentTestNGIReporterListener"></listener>
</listeners>
</suite>

6、测试运行方式

  1. IDEA工具直接执行testng.xml(以testng形式运行)即可(IDEA工具需要先装好testng插件)
  2. maven执行:根目录下,执行 mvn test

7、测试报告呈现

  1. testng.xml执行可视化报告:${workspace}/test-output/index.html
  2. maven执行报告:${workspace}/target/test-output/index.html

基于maven+java+TestNG+httpclient+poi+jsonpath+ExtentReport的接口自动化测试框架的更多相关文章

  1. Maven+TestNG+ReportNG/Allure接口自动化测试框架初探(上)

    转载:http://www.51testing.com/html/58/n-3721258.html 由于一直忙于功能和性能测试,接口自动化测试框架改造的工作被耽搁了好久.近期闲暇一些,可以来做点有意 ...

  2. 接口自动化 [授客]基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0

    基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0   by:授客 QQ:1033553122     博客:http://blog.sina.com.cn/ishou ...

  3. 接口自动化 基于python+Testlink+Jenkins实现的接口自动化测试框架[V2.0改进版]

    基于python+Testlink+Jenkins实现的接口自动化测试框架[V2.0改进版]   by:授客 QQ:1033553122 由于篇幅问题,,暂且采用网盘分享的形式: 下载地址: [授客] ...

  4. 基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0

    基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0 目录 1. 开发环境2. 主要功能逻辑介绍3. 框架功能简介 4. 数据库的创建 5. 框架模块详细介绍6. Tes ...

  5. 接口自动化 基于python实现的http+json协议接口自动化测试框架源码(实用改进版)

    基于python实现的http+json协议接口自动化测试框架(实用改进版)   by:授客 QQ:1033553122 欢迎加入软件性能测试交流QQ群:7156436     目录 1.      ...

  6. Python 基于python实现的http接口自动化测试框架(含源码)

    基于python实现的http+json协议接口自动化测试框架(含源码) by:授客 QQ:1033553122      欢迎加入软件性能测试交流 QQ群:7156436  由于篇幅问题,采用百度网 ...

  7. 基于Python接口自动化测试框架+数据与代码分离(进阶篇)附源码

    引言 在上一篇<基于Python接口自动化测试框架(初级篇)附源码>讲过了接口自动化测试框架的搭建,最核心的模块功能就是测试数据库初始化,再来看看之前的框架结构: 可以看出testcase ...

  8. 基于Python的HTTP接口自动化测试框架实现

    今天我们来讲一下基于Python的HTTP接口自动化测试框架的实现,范例如下: 一.测试需求描述 对服务后台一系列的http接口功能测试. 输入:根据接口描述构造不同的参数输入值 输出:XML文件 e ...

  9. Python 基于python实现的http+json协议接口自动化测试框架源码(实用改进版)

    目录 1.      写在前面 2.      开发环境 3.      大致流程 4.      框架简介 5.      运行结果展示 6.      文件与配置 7.      测试接口实例 n ...

随机推荐

  1. PP: Imaging time-series to improve classification and imputation

    From: University of Maryland encode time series as different types of images. reformulate features o ...

  2. Docker镜像加速-配置阿里云镜像仓库

    Docker默认远程仓库是https://hub.docker.com/ 比如我们下载一个大点的东西,龟速 由于是国外主机,类似Maven仓库,慢得一腿,经常延迟,破损: 所以我们一般都是配置国内镜像 ...

  3. intermediate-python-for-data-science

    目录 matplotlib plot Scatter Plot histogram Customization Dictionaries, Part 1 Create dictionary dicti ...

  4. 0009 注册登录(第二步:获取短信接口access token)

    1 在GeneralTools目录下创建一个常量文件Constants.py 获取短信验证之前需要申请腾讯云短信服务. """ 腾讯云短信相关常量 "" ...

  5. AcWing 187. 导弹防御系统

    //dp+dfs+贪心 //记一个全局变量 #include<iostream> using namespace std ; ; int n; int ans; int q[N]; int ...

  6. INEQUALITY BOOKS

    来源:这里 Bất Đẳng Thức Luôn Có Một Sức Cuốn Hút Kinh Khủng, Một Số tài Liệu và Sách Bổ ích Cho Việc Học ...

  7. jQuery jqgrid 应用实例

    1.html <div class="ibox-content"> <div class=\"jqGrid_wrapper\"> < ...

  8. Android基础知识 -- Fragment

    Fragment是android3.0后提供的API(所以android:minSdkVersion="11"以上版本),主要针对平板UI.有自己的生命周期,但是必须依附在Acti ...

  9. Rumor

    Vova promised himself that he would never play computer games... But recently Firestorm — a well-kno ...

  10. E. Vus the Cossack and a Field (求一有规律矩形区域值) (有一结论待证)

    E. Vus the Cossack and a Field (求一有规律矩形区域值) 题意:给出一个原01矩阵,它按照以下规则拓展:向右和下拓展一个相同大小的 0 1 分别和原矩阵对应位置相反的矩阵 ...