思路梳理
- 使用 java 类来梳理 case
- 使用 excel + pojo 来抽取封装用例数据
- 对excel 与 pojo类进行设计以 适应不同协议的接口,携带请求头信息 等
- 使用 testng aseert 封装的工具 来进行测试断言
- 使用 基于httpClient 封装的工具 来进行发送请求
- 使用 mvn 整合 allurereport 完成测试结果展示
使用 moco 来对一些未完成的接口进行 mock 减少测试工程整体依赖,实现组间并行作业。
项目整体工程目录
数据驱动实现
因为testng的数据驱动实现起来比python要麻烦很多 ,这里尝试过多种方案,
- 最初 考虑使用ali的 easyExcel 但是easyExcel 依赖springboot
- 后考虑使用 poi 但是可读性很差
- 最后定稿决定使用 easyPoi 的方式读取excel 配合 注解完成数据的读取与dump封装完成数据驱动 ```java package com.addicated.utils;
import cn.afterturn.easypoi.excel.ExcelImportUtil; import cn.afterturn.easypoi.excel.entity.ImportParams; import cn.afterturn.easypoi.handler.inter.IExcelVerifyHandler; import com.addicated.pojo.Case; import com.addicated.pojo.Rest;
import java.io.; import java.util.;
/**
重新编写excel的工具类来完成数据驱动操作 */ public class ExcelUtils {
/**
- 读取excel数据后封装到指定对象中 *
- @param sheetIndex 开始sheet索引
- @param sheetNum sheet个数
- @param clazz excel映射类字节对象
@return */ public static List read(String path,int sheetIndex, int sheetNum, Class clazz) { try {
// 1. excel文件流 FileInputStream fis = new FileInputStream(path); // 2. easypoi 导入参数 ImportParams params = new ImportParams(); params.setStartSheetIndex(sheetIndex);//从第x个sheet开始读取 params.setSheetNum(sheetNum);//读取x个sheet
// params.setNeedVerify(true); // IExcelVerifyHandler excelHandler = new ExcelHandler(); // params.setVerifyHandler(excelHandler);
params.setTitleRows(1); // 3. 导入 List caseInfoList = ExcelImportUtil.importExcel(fis, clazz, params); // 4. 关流 fis.close(); return caseInfoList;
} catch (Exception e) {
e.printStackTrace();
} return null; } }
- 代码层面十分简洁,但对应的前期准备工作相对较多。
- 使用的excel文件columns信息大致如下,本次案例学习研究性质更重一些,仅提供思路参考
- 
<a name="nWqgc"></a>
## pojo实体类的准备
- 下见代码,省略 getter/setter
```java
package com.addicated.pojo;
import cn.afterturn.easypoi.excel.annotation.Excel;
/**
* PS:用例实体类
* 该框架设计中
* 对应excel中的用例记录,方便读取之后映射
*
* @author lkk
*/
public class Case {
// 接口编号
@Excel(name = "ApiNum(接口编号)")
private String apiNum;
// 用例编号
@Excel(name = "CaseNum(用例编号)")
private String caseNum;
// 用例名
@Excel(name = "CaseName(用例名)")
private String caseName;
// 请求头部信息 RequestHeaders(请求头部)
@Excel(name = "RequestHeaders(请求头部)")
private String requestHeaders;
@Excel(name = "Protocol(请求协议)")
private String protocol;
// 请求数据类型
@Excel(name = "RequestDataType(请求数据类型)")
private String requestDataType;
// 请求数据 RequestData(请求数据)
@Excel(name = "RequestData(请求数据)")
private String requestData;
@Excel(name = "RequestType(请求类型)")
private String requestType;
// 检查点 CheckPoint(检查点)
@Excel(name = "CheckPoint(检查点)")
private String checkPoint;
// 预期响应结果 ExpectResponseResult(预期响应结果)
@Excel(name = "ExpectResponseResult(预期响应结果)")
private String expectResponseResult;
// 实际响应结果
@Excel(name = "ActualResponseResult(实际响应结果)")
private String actualResponseResult;
// 关联字段 Correlation(关联字段)
@Excel(name = "Correlation(关联字段)")
private String correlation;
@Excel(name = "RequestUrl(请求路径)")
private String requestUrl;
public String getRequestUrl() {
return requestUrl;
}
public void setRequestUrl(String requestUrl) {
this.requestUrl = requestUrl;
}
// 检查SQL语句
private String checkExcuteSql;
// 执行前结果
private String pre_ExecutionResults;
// 执行后结果
private String post_ExecutionResults;
}
- 由此,可以java读取Excel 到封装成为 Objects类型list集合的操作
- 下见case中实际使用 ```java package com.addicated.test_case;
import com.addicated.asserts.assertUtil; import com.addicated.httpUtils.Request; import com.addicated.httpUtils.Response; import com.addicated.pojo.Case; import com.addicated.utils.ExcelUtils; import com.addicated.wework.Wework; import org.apache.http.HttpResponse; import org.apache.http.client.HttpResponseException; import org.apache.http.client.methods.CloseableHttpResponse; import org.testng.annotations.DataProvider; import org.testng.annotations.Test;
import java.util.List;
/**
- 企业微信
通讯录 - 部门api 测试用 */ public class API_001_Department { public static CloseableHttpResponse response;
@Test(dataProvider = “datas”, testName = “rest信息读取测试”) public void baseTest(Case testcase) {
/** * 思考 尽量使用一个sheet 来完成case需要的数据 * 改造excel中的case数据比整合代码要来的省力 */ // 获取企业微信的token String token = Wework.getToken(); // 根据传入数据判断接口方法 if (testcase.getApiNum() != null) { switch (testcase.getRequestType()) { case "GET": response = Request.sendGetRequest(testcase.getProtocol(), token, testcase.getRequestUrl(), testcase.getRequestData()); } // 断言 String result = Response.getResponseEntity(response); assertUtil.check(result,testcase.getExpectResponseResult()); String responseEntity = Response.getResponseEntity(response); System.out.println(responseEntity); }else { }
}
@DataProvider public Object[] datas() {
// 多张sheet联读拼接成为最终case需要的数据 // 传入 path,sheetnum ,读取sheet数 ,要影射封装成的实体类对象字节码文件。 List list = ExcelUtils.read("src/main/resources/data/wework_case.xlsx", 0, 1, Case.class); // 返回一个object类型的list集合 return list.toArray();
}
}
<a name="dobjT"></a>
# 请求工具类的封装思路
- httpclient的原生类十分的繁琐,对自动化相当不友好,所以需要进行封装
- 在进行封装的时候,需要思考几个问题
- 要做什么类型的请求?
- 是否支持请求头的拼接
- 适配多种协议的接口
- 适配加解密接口
- 适配多参数接口
- 适配庞大json请求体的表单接口
- 抽取共有的部分进行url地址拼接
- 做多个重载来应对不同情况的调用
- 观察被测主体的url规律,是否需要token以及拼接规则等等

```java
package com.addicated.httpUtils;
import com.addicated.config.WeworkConfig;
import com.addicated.log.Log;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.testng.util.Strings;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
/**
* 进一步对httpClient的api进行封装
* 并且适配多种环境,协议
*/
public class Request {
// httpClient客户端饮用
public static CloseableHttpClient httpClient;
/**
* 拼接协议 + 访问地址 以适配不同协议的接口
*
* @param protocol
* @param url
* @return
*/
public static String groupHostAndParams(String protocol, String url) {
url = protocol + "://" + url;
return url;
}
/**
* 拼接,路径,以及参数
*
* @param protocol
* @param url
* @param params
* @return
*/
public static String groupHostAndParams(String protocol, String token, String url, String params) {
//拼接协议及请求地址
url = protocol + "://" + WeworkConfig.getInstance().baseUrl + url + "access_token=" + token;
if (!Strings.isNullOrEmpty(params)) {
//将参数转为map
Map<String, String> paramsMap = (Map<String, String>) JSONObject.parse(params);
//获取map的key集合
Set<String> keySet = paramsMap.keySet();
//计数器
int i = 1;
//遍历key值获取value并拼接到地址上
for (String key : keySet) {
// if (i == 1) {
// url = url + "?" + key + "=" + paramsMap.get(key);
// } else {
url = url + "&" + key + "=" + paramsMap.get(key);
// }
i++;
}
}
return url;
}
/**
* 封装发送get请求方法
*
* @param requestHeaders 头信息
* @param protocol 协议版本
* @param url 访问地址
* @param params 参数
* @return 响应对象
*/
public static CloseableHttpResponse sendGetRequest(String requestHeaders,
String token,
String protocol,
String url,
String params) {
//拼接地址 调用该本类的静态方法完成url拼接
// groupHostAndParams 方法在本类中有多个重载方式,根据传入参数不同,调用方法不同
url = groupHostAndParams(url, params);
// 创建get请求对象
HttpGet httpGet = new HttpGet(url);
Map<String, String> HeadersMap = (Map<String, String>) JSONObject.parse(requestHeaders);
// 遍历map将头信息设置到请求中
for (String key : HeadersMap.keySet()) {
httpGet.setHeader(key, HeadersMap.get(key));
}
// 发送后请求获取响应对象 仅声明
CloseableHttpResponse httpResponse = null;
try {
httpResponse = httpClient.execute(httpGet);
} catch (IOException e) {
Log.error("发送get请求失败");
}
return httpResponse;
}
public static CloseableHttpResponse sendGetRequest(
String protocol,
String token,
String url,
String params) {
//拼接地址 调用该本类的静态方法完成url拼接
// groupHostAndParams 方法在本类中有多个重载方式,根据传入参数不同,调用方法不同
url = groupHostAndParams(protocol, token, url, params);
// 创建get请求对象
HttpGet httpGet = new HttpGet(url);
// Map<String, String> HeadersMap = (Map<String, String>) JSONObject.parse(requestHeaders);
// // 遍历map将头信息设置到请求中
// for (String key : HeadersMap.keySet()) {
// httpGet.setHeader(key, HeadersMap.get(key));
// }
// 发送后请求获取响应对象 仅声明
CloseableHttpResponse httpResponse = null;
HttpClient client = HttpClients.createDefault();
try {
httpResponse = (CloseableHttpResponse) client.execute(httpGet);
} catch (IOException e) {
Log.error("发送get请求失败");
}
return httpResponse;
}
/**
* post请求封装
*
* @param requestHeaders
* @param protocol
* @param url
* @param requestDataType
* @param params
* @return
*/
public static CloseableHttpResponse sendPostRequest(String requestHeaders,
String protocol,
String url,
String requestDataType,
String params) {
//拼接URL地址
url = groupHostAndParams(protocol, url);
//创建post请求对象
HttpPost httpPost = new HttpPost(url);
//将头部信息字符串转为map
Map<String, String> HeadersMap = (Map<String, String>) JSONObject.parse(requestHeaders);
//遍历map将头部信息设置到请求中
for (String key : HeadersMap.keySet()) {
httpPost.setHeader(key, HeadersMap.get(key));
}
CloseableHttpResponse httpResponse = null;
try {
//判断请求类型
if (requestDataType.equals("json")) {
httpPost.setEntity(new StringEntity(params));
} else if (requestDataType.equals("file")) {
//获取指定路径文件
File file = new File(params);
//将文件对象转为文件实体对象
FileEntity fileEntity = new FileEntity(file);
//发送请求
httpPost.setEntity(fileEntity);
} else {
}
httpResponse = httpClient.execute(httpPost);
} catch (Exception e) {
Log.error("发送POST请求失败");
}
return httpResponse;
}
/**
* PS:发送put请求
*
* @param requestHeaders 头部信息
* @param protocol 协议版本
* @param url 访问地址
* @param params 参数
* @return 响应对象
*/
public static CloseableHttpResponse sendPutRequest(String requestHeaders,
String protocol,
String url,
String params) {
//拼接URL地址
url = groupHostAndParams(protocol, url);
//创建put请求对象
HttpPut httpPut = new HttpPut(url);
//将头部信息字符串转为map
Map<String, String> HeadersMap = (Map<String, String>) JSONObject.parse(requestHeaders);
//遍历map将头部信息设置到请求中
for (String key : HeadersMap.keySet()) {
httpPut.setHeader(key, HeadersMap.get(key));
}
CloseableHttpResponse httpResponse = null;
try {
//设置主体信息
httpPut.setEntity(new StringEntity(params));
httpResponse = httpClient.execute(httpPut);
} catch (Exception e) {
Log.error("发送PUT请求失败");
}
return httpResponse;
}
/**
* @param requestHeaders
* @param protocol
* @param url
* @param params
* @return
*/
public static CloseableHttpResponse sendDeleteRequest(String requestHeaders,
String protocol,
String url,
String params) {
// 拼接请求地址
url = groupHostAndParams(url, params);
// 创建Delete请求对象
HttpDelete httpDelete = new HttpDelete(url);
//将头部信息字符串转为map
Map<String, String> HeadersMap = (Map<String, String>) JSONObject.parse(requestHeaders);
//遍历map将头部信息设置到请求中
for (String key : HeadersMap.keySet()) {
httpDelete.setHeader(key, HeadersMap.get(key));
}
// 发送请求获取响应对象
CloseableHttpResponse httpResponse = null;
try {
httpResponse = httpClient.execute(httpDelete);
} catch (IOException e) {
Log.error("发送DELETE请求失败");
}
return httpResponse;
}
/**
* PS:发送请求服务
* <p>
* 整体性封装,通过一个入口判断调用多种http请求方法,
*
* @param method 请求方式
* @param requestHeaders 头部信息
* @param protocol 协议版本
* @param url 访问地址
* @param params 参数
* @return 响应对象
*/
public static CloseableHttpResponse doServer(String method, String requestHeaders, String protocol, String url, String requestDataType, String params) {
//获取http客户端对象
httpClient = HttpClients.createDefault();
//cookie保存对象
//CookieStore cookieStore = new BasicCookieStore();
//发送对象设置cookie保存机制
//httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
//根据请求方式选择方法发送请求
if ("GET".equals(method.toUpperCase())) {
return sendGetRequest(requestHeaders, protocol, url, params);
} else if ("POST".equals(method.toUpperCase())) {
return sendPostRequest(requestHeaders, protocol, url, requestDataType, params);
} else if ("PUT".equals(method.toUpperCase())) {
return sendPutRequest(requestHeaders, protocol, url, params);
} else {
return sendDeleteRequest(requestHeaders, protocol, url, params);
}
}
}
响应工具类的封装思路
- 与上面请求工具类的封装类似 httpclient原生使用起来有诸多不变
- 那么封装思路分析一下,都有那些东西需要为自动化服务进行返回
- 状态码
- 响应体字符串
- 响应头信息 ```java package com.addicated.httpUtils;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.commons.codec.binary.StringUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.ParseException; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.util.EntityUtils;
import com.addicated.log.Log;
import java.io.IOException; import java.util.HashMap; import java.util.Map;
/**
获取响应体的工具类 / public class Response { /*
- PS:获取响应头部信息
- @param httpResponse 响应对象
@return 响应头字典 */ public static Map
getResponseHeaders(CloseableHttpResponse httpResponse) { //存储响应头信息的Map Map responseHeaders = new HashMap(); //获取响应头对象数组 Header[] headers = httpResponse.getAllHeaders(); //遍历响应头数组将key,value塞入map for(Header header:headers) { responseHeaders.put(header.getName(), header.getValue());
} return responseHeaders; }
/**
- 获取响应主体信息
- @param httpResponse
- @return 响应主体信息字符串
*/
public static String getResponseEntity(CloseableHttpResponse httpResponse) {
//最终返回的字符串信息
String responseEntity = null;
//获取主体信息对象
HttpEntity entity = httpResponse.getEntity();
try {
} catch (ParseException e) {//将主体信息对象转为字符串 responseEntity = EntityUtils.toString(entity, "UTF-8");
} catch (IOException e) {Log.error("+++++响应主体信息转换异常+++++");
} return responseEntity; }Log.error("+++++响应主体信息IO异常+++++");
/**
* PS:获取响应码
* @param httpResponse 响应对象
* @return 状态码
*/
public static int getResponseCode(CloseableHttpResponse httpResponse) {
//获取响应头对象
StatusLine statusLine = httpResponse.getStatusLine();
//获取响应码
int responseCode = statusLine.getStatusCode();
return responseCode;
}
}
<a name="oSH2F"></a>
# 断言工具类的封装思路
- 是的 没错,断言也要进行封装 ,java就是个封装
- 同样,梳理一下 都有哪几种常见断言需要搞
- 全等
- 包含
- 正则
- 同时添加上log记录
```java
package com.addicated.asserts;
import com.addicated.log.Log;
import org.testng.Assert;
public class assertUtil extends Assert {
/**
* 是否包含断言信息
*
* @param target
* @param assertion
* @return
*/
public static boolean check(String target,
String assertion) {
// 测试目标中是否包含期望结果
boolean flag = target.contains(assertion);
if(flag==true) {
Log.info("-----包含断言通过,检测响应部分包含断言["+assertion+"]-----");
}else {
flag = false;
throw new AssertionError("-----包含断言失败,检测响应部分["+target+"]未包含断言["+assertion+"]-----");
}
return flag;
}
/**
* 是否等于断言信息
* @param target
* @param assertion
*/
public static void equals(String target,
String assertion) {
boolean flag = assertion.equals(target);
if(flag == true) {
Log.info("-----比较断言通过,检测响应部分等于断言["+assertion+"]-----");
}else {
throw new AssertionError("-----比较断言失败,检测响应部分不等于断言["+assertion+"]-----");
}
}
/**
* 是否匹配正则表达式
* @param target
* @param assertionRegex
*/
public static void matches(String target,
String assertionRegex) {
boolean flag = target.matches(assertionRegex);
if(flag == true) {
Log.info("-----正则匹配断言通过,检测响应部分["+target+"]匹配断言正则表达式["+assertionRegex+"]-----");
}else {
throw new AssertionError("-----正则匹配断言未通过,检测响应部分["+target+"]不匹配断言正则表达式["+assertionRegex+"]-----");
}
}
/**
* 开头匹配断言
* @param target
* @param assertion
*/
public static void startsWith(String target,String assertion) {
boolean flag = target.startsWith(assertion);
if(flag == true) {
Log.info("-----检测开头断言通过,检测响应部分["+target+"]的头部是断言["+assertion+"]-----");
}else {
throw new AssertionError("-----检测开头断言未通过,检测响应部分["+target+"]的头部不是断言["+assertion+"]-----");
}
}
/**
* PS:结尾匹配断言
* @param target
* @param assertion
* @throws assertException
*/
public static void endsWith(String target,String assertion){
boolean flag = target.endsWith(assertion);
if(flag == true) {
Log.info("-----检测结尾断言通过,检测响应部分["+target+"]的结尾是断言["+assertion+"]-----");
}else {
throw new AssertionError("-----检测结尾断言未通过,检测响应部分["+target+"]的结尾不是断言["+assertion+"]-----");
}
}
}
Log类封装 - api自动化运行的日志记录支持
- 说到自动化自然是少不了运行日志记录的,不然哪个case失败了,为什么失败了,都无从复盘。
- 使用log4j 进行日志记录 ```java package com.addicated.log;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
/**
- 测试用例执行log记录 */ public class Log {
private static Logger Log = LogManager.getLogger(Log.class.getName());
/**
* 用例开始日志
*/
public static void startTestCase() {
String classname = new Exception().getStackTrace()[1].getClassName();
Log.info(" --------用例" + classname + "开始执行 --------");
}
/**
* 用例结束日志
*/
public static void endTestCase() {
String classname = new Exception().getStackTrace()[1].getClassName();
Log.info(" --------用例" + classname + "执行结束 --------");
}
/**
* 根据内容输出日志
*
* @param message
*/
public static void info(String message) {
Log.info(message);
}
public static void warn(String message) {
Log.warn(message);
}
public static void error(String message) {
Log.error(message);
}
public static void fatal(String message) {
Log.fatal(message);
}
public static void debug(String message) {
Log.debug(message);
}
}
// 在各个工具类中加入log日志, 在发生异常的时候进行日志记录。
<a name="Fe4oc"></a>
# 失败重试
- 本次并未实现,但摘录网上流传的实现方式以供参考
[https://www.cnblogs.com/lv-suma/p/12585384.html](https://www.cnblogs.com/lv-suma/p/12585384.html)
<a name="cSaC2"></a>
# moco的使用
- 主要用来实现尚未开发完成的接口返回,或者第三方接口依赖
[https://www.cnblogs.com/Sweettesting/p/13860291.html](https://www.cnblogs.com/Sweettesting/p/13860291.html)
- 具体在本文不再进行摘录,直接贴外链
<a name="M1i1T"></a>
# 测试报告的生成
```java
cd maven项目路径
mvn clean test
allure serve target/allure-results
代码地址
- 当前为一个粗略的版本,不排除后续会对其进行迭代加强。