目录
经过本次代码重构,以上原本复杂的,对外部文件解析执行的核心处理类,逻辑变得非常简单
完整案例之代码实现:如果要写代码完成测试案例,DSL 的设计让开发者事半功倍,让看代码的人通俗易懂
前言:
分层测试是一种测试方法,它将软件分为多个层次进行测试,从而提高测试的效率和准确性。在分层测试中,接口层是非常重要的一部分。接口层测试是测试应用程序的 API 接口是否能够正确地响应和处理请求。
接口层优化需求
总体目标
- 支持基于 Http 协议的所有测试,包含但不仅限于接口测试,例如:Web 测试中对于页面的请求,上传和下载资源等操作
- 基于行为驱动开发的思想,支持外部 DSL 和内部 DSL
- 用户可调用 API 开发案例,也可以不写代码按通俗易懂的既定格式编写案例
- 结构设计清晰,利于扩展
- 测试流核心模块以外,开放接口,支持用户在不改变框架源码的基础上实现定制化(针对高级用户)
实现功能
- 支持包括 rest 风格在内的各种形式的基于 http 协议的请求
- 支持各种请求场景的参数化,包括但不仅限于:url、path、表单等
- 支持通过正则表达式提取动态参数,支持动态参数的脚本内调用和跨脚本调用
- 支持通过加载外部文件、调用内部方法、调用自定义方法等多种方式实现参数化的添加
- 支持自定义 header,实现伪造
- 支持 json、xml、x-www-form-urlencoded 等请求参数格式
- 支持断言的普通全匹配、正则表达式的全匹配、普通包含匹配、使用正则表达式的包含匹配、使用变量进行参数化匹配等
- 支持客户端处理生成的请求参数,可以通过反射调用用户自定义的方法
- 支持请求配置项的全局配置和局部配置,局部配置的优先级大于全局配置,即使用局部配置全局配置的相同配置项失效
具体实现
如何通过文件写一个接口测试
设计采用 json 格式,一方面是方便对结构的正确性进行校验,另一方面考虑到大部分数据传输都用 json 格式,便于推广。
配置字段说明:
- testcase:案例名称 ,选配字段
- scope:模式,必配字段,1 代表接口层,2 代表数据层,4 代表 UI 层,3 代表接口层 + 数据层,以此推类
- defaultEncode:默认 UTF-8,全局编码,选配字段
- useHttps:是否支持 Https 的开关,默认 false,目前支持安全系数较低的 SHA-1 签名,选配字段
- useProxy:全局代理配置,选配字段,例如:xx.xx.xx.xx:8080
- redirect:重定向开关,默认 false,选配字段
- defaultHeader:全局头信息,选配字段,多个数据用双逗号分隔,字段和属性以双冒号分隔,例如:Content-Type::application/x-www-form-urlencoded,,accept-encoding::gzip
- globalData:全局参数,选配字段,多个参数逗号分隔,例如:userLogin=UserLogin,rsp=Rsp,name=jack
- iterativeData:全局参数,支持多条数据的参数化、支持内部配置读取和外部文件读取。 内部读取:以 inner 开头,多条数据以分号分隔,字段用逗号分隔,例如:inner a=1,b=2;a=2,b=3;a=3,b=4, 外部读取:以 outer 开头,file 指定文件路径,fields 指定参数化的字段,separate 指定分隔符,三个配置用&分隔, 例如:outer file=src/parameters&fields=phone,clientType&separator=:
- step:测试步骤,选配字段
- url:请求地址,必配字段,支持强大的参数化功能,例如:
(http://xx.xx.xx.xx/app/testGet.do)
,(http://xx.xx.xx.xx/${pram}/testGet.do)
,支持客户端定制化生成的参数:(http://xx.xx.xx.xx/${pram}/testGet.do?field1=#{pers.quq.layer.tools.DateUtil.test(1,2)})
,如果动态方法无参,则方法名右边不需要括号 - method:请求方法,必填字段,大小写不敏感,例如:GET、Post
- contentType:请求内容类型,选填字段,例如:application/json、text/xml、application/x-www-form-urlencoded、multipart/form-data(该项需配置fileParam)
- param:请求参数,选配字段,例如:user_phone=jack&date=20160303,同样支持强大的参数化功能,例如:user_phone=${user_phone}&date=#{pers.quq.layer.tools.DateUtil.test}
- header:局部头信息,选配字段,相同的属性会覆盖全局全局头信息的属性,配置同 defaultHeader
- encode:局部编码,选配字段,会覆盖全局编码
- proxy:局部代理配置,选配字段,会覆盖全局代理配置,配置请参考 useProxy
- regex:正则表达式提取器,选配字段,小括号内的内容就是动态获取的值,会保存到等号左边指定的字段, 字段={左边界 (正则表达式) 右边界}# 取第几个, 例如:msg={msg\":\"(.*)\",\"result}#1
- shouldBeContains:断言之普通包含匹配,选配字段,支持参数化,例如:aa${param}bb
- shouldBeEquals:断言之普通全匹配,选配字段,支持参数化,配置请参考 shouldBeContains
- shouldBeContainsByRegex:断言之使用正则表达式的包含匹配,选配字段,支持参数化,例如:msg\":\".*\",\"result
- shouldBeEqualsByRegex:断言之使用正则表达式的全匹配,选配字段,支持参数化,配置请参考 shouldBeContainsByRegex
- delay:请求结束后的延时,单位秒,选配字段
- fileParam:上传文件的请求参数,选配字段,例如:文件参数名 1=文件路径 1,文件参数名 2=文件路径 2。。。。。。
raw 方式传参:json 格式
{
"testcase": "layTestCase",
"scope": 1,
"useHttps": true,
"redirect": true,
"globalData": "user=quqing,password=123,addr1=803,addr2=831",
"defaultEncode": "UTF-8",
"requests": [
{
"step": 1,
"desc": "删除收货地址",
"header": "Authorization::Bearer ${token}",
"url": "http://xx.xx.xx.xx:8081/addr/${addr2}",
"method": "delete",
},
{
"step": 2,
"desc": "修改商品数量",
"header": "Authorization::Bearer ${token}",
"param": "shoppingCartId=${shoppingCartId}&goodsNum=2",
"url": "http://xx.xx.xx.xx:8081/update",
"method": "put",
"shouldBeEquals": "{\"code\":\"K-000000\",\"message\":\"操作成功\"}"
}
{
"step": 3,
"desc": "删除购物车单个商品",
"header": "Authorization::Bearer ${token},,Content-Type::application/json",
"url": "http://xx.xx.xx.xx:8081/goods",
"method": "delete",
"param": "{\"shoppingCartId\":8022}",
}
]
}
普通方式传参
{
"scope": 1,
"useHttps": true,
"redirect": true,
"globalData": "user=quqing,password=123,goodsInfoId=6666",
"defaultEncode": "UTF-8",
"defaultHeader": "Content-Type::application/x-www-form-urlencoded",
"requests": [
{
"step": 1,
"desc": "登录",
"url": "http://xx.xx.xx.xx:8081/checkLogin",
"method": "post",
"param": "user=${user}&password=${password}",
"encode": "UTF-8",
"regex": "token={\"token\":\"(.*)\"}#1",
"shouldBeContains": "\"token\":"
},
{
"step": 2,
"desc": "加入购物车单个商品",
"header": "Authorization::Bearer ${token}",
"param": "districtId=1&goodsInfoId=${goodsInfoId}&goodsNum=1",
"url": "http://xx.xx.xx.xx:8081/goods",
"method": "post",
"shouldBeEquals": "{\"code\":\"K-000000\",\"message\":\"操作成功\"}"
},
{
"step": 3,
"desc": "获取购物车列表",
"header": "Authorization::Bearer ${token}",
"url": "http://xx.xx.xx.xx:8081/list?districtId=1",
"method": "get",
"regex": "shoppingCartId={\"shoppingCartId\":([0-9]+),\"goodsInfoId\"}#2",
"shouldBeContains": "\"productResponseList\":[{\"shoppingCartId\":"
},
{
"step": 4,
"desc": "下单",
"header": "Authorization::Bearer ${token}",
"param": "shoppingCartId=${shoppingCartId}&addressId=803&message=hello",
"url": "http://xx.xx.xx.xx:8081/submit",
"regex": "orderId={\"orderId\":(.*),\"orderCodes\"}#1",
"method": "post",
"shouldBeNotContains": "\"orderId\": null",
"shouldBeNotContainsByRegex": "\"code\":\"K-.*\",\"message\""
},
{
"step": 5,
"desc": "查询订单基本信息",
"header": "Authorization::Bearer ${token}",
"url": "http://xx.xx.xx.xx:8080/${orderId}",
"method": "get",
"shouldBeNotContains": "\"orderId\": null1",
"shouldBeNotContainsByRegex": "\"code\":\"K-.*\",\"message\""
}
]
}
经过本次代码重构,以上原本复杂的,对外部文件解析执行的核心处理类,逻辑变得非常简单
package pers.quq.layer.tester;
import pers.quq.layer.entity.LayerCaseDO;
import pers.quq.layer.entity.RequestDO;
import pers.quq.layer.exception.*;
import pers.quq.layer.jiekou.HttpTester;
import pers.quq.layer.jiekou.ParamsProxy;
import pers.quq.layer.jiekou.def.HttpContentType;
import pers.quq.layer.jiekou.def.HttpMethod;
import pers.quq.layer.jiekou.def.IHttpRequest;
import pers.quq.layer.jiekou.impl.HttpConfig;
import pers.quq.layer.jiekou.impl.HttpRequest;
import pers.quq.layer.parse.DataProcess;
import pers.quq.layer.parse.Parse;
import pers.quq.layer.parse.ParseLayerCase;
import pers.quq.layer.tools.FileUtil;
import pers.quq.layer.tools.Log;
import pers.quq.layer.tools.ParseProperties;
import pers.quq.layer.tools.Persistence;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
/**
* Created by quqing on 2016/3/2.
*/
public class TestInterface {
private String response = null;
private String defaultEncode;
private String requestBody;
private String contentType;
private Map<String, String> defaultHeader;
private Map<String, String> header;
private Map<String, String> globalData;
private Map<String, Object> fileParam;
private Map<String, Object> paramsMap = null;
private List<Map<String, String>> iterativeParamList;
private IHttpRequest httpRequest;
private Parse parseLayerCase = new ParseLayerCase();
private LayerCaseDO layerCaseDO;
private List<RequestDO> requests;
private ParseProperties config = new ParseProperties();
private void init(String testCase) throws Exception {
layerCaseDO = (LayerCaseDO) parseLayerCase.parse(testCase);
defaultEncode = null != layerCaseDO.getDefaultEncode() ? layerCaseDO.getDefaultEncode() : "UTF-8";
defaultHeader = null != layerCaseDO.getDefaultHeader() ? DataProcess.header(layerCaseDO.getDefaultHeader()) : null;
globalData = DataProcess.globalData(layerCaseDO.getGlobalData());
requests = layerCaseDO.getRequests();
iterativeParamList = null != layerCaseDO.getIterativeData() ? DataProcess.iterativeData(layerCaseDO.getIterativeData()) : null;
Log.logInfo("************************************************************************************************************************************************************************************************************");
Log.logInfo("defaultEncode: " + layerCaseDO.getDefaultEncode());
Log.logInfo("defaultHeader: " + layerCaseDO.getDefaultHeader());
Log.logInfo("globalData: " + layerCaseDO.getGlobalData());
Log.logInfo("iterativeData: " + layerCaseDO.getIterativeData());
Log.logInfo("useHttps: " + layerCaseDO.getUseHttps());
Log.logInfo("useProxy: " + layerCaseDO.getUseProxy());
Log.logInfo("************************************************************************************************************************************************************************************************************");
HttpConfig httpConfig = HttpConfig.custom()
.setDefaultEncode(defaultEncode)
.setHeader(defaultHeader)
.setRedirectStrategy(layerCaseDO.getRedirect())
.setHttps(layerCaseDO.getUseHttps())
.setProxy(layerCaseDO.getUseProxy())
.build();
httpRequest = (IHttpRequest) ParamsProxy.newInstance(new HttpRequest(httpConfig), globalData);
}
private void run(String resultDir) throws DataProcessException, UnsupportedEncodingException, URISyntaxException, TestInterfaceException {
String desc;
String oldHeader;
File dir;
Map<String, String> persistParamMap;
if (Persistence.isFileExist()) {
persistParamMap = Persistence.persisting_load();
if (null == globalData) {
globalData = persistParamMap;
} else {
globalData.putAll(persistParamMap);
}
}
for (RequestDO requestDO : requests) {
desc = requestDO.getDesc().trim();
oldHeader = requestDO.getHeader();
// header处理
if (null != oldHeader) {
if (null != globalData && globalData.size() > 0) {
for (String key : globalData.keySet()) {
oldHeader = oldHeader.replaceAll("\\$\\{" + key + "\\}", globalData.get(key));
}
}
}
header = null != oldHeader ? DataProcess.header(oldHeader) : null;
// Content-Type处理
if (null == header) {
contentType = HttpContentType.SC_FROM_URL_ENCODED;
} else {
if (null == header.get("Content-Type")) {
contentType = HttpContentType.SC_FROM_URL_ENCODED;
} else {
contentType = header.get("Content-Type");
}
}
fileParam = DataProcess.param(requestDO.getFileParam(), layerCaseDO.getDefaultEncode());
if (null != requestDO.getEncode())
httpRequest.setDefaultEncode(requestDO.getEncode());
httpRequest.setHeader(header);
httpRequest.setContentType(contentType);
httpRequest.setProxy(requestDO.getProxy());
if (contentType.contains(HttpContentType.SC_JSON) || contentType.contains(HttpContentType.SC_XML)) {
requestBody = null != requestDO.getParam() ? requestDO.getParam().trim() : null;
} else {
paramsMap = DataProcess.param(requestDO.getParam(), layerCaseDO.getDefaultEncode());
}
if (null != requestDO.getFileParam()) {
response = httpRequest.doPost(requestDO.getUrl(), paramsMap, fileParam);
} else if (contentType.contains(HttpContentType.SC_RESOURCE)) {
response = httpRequest.doGet(requestDO.getUrl(), paramsMap, config.get("downloadDir") + System.currentTimeMillis());
} else if (requestDO.getMethod().equalsIgnoreCase(HttpMethod.Post.toString())) {
if (contentType.contains(HttpContentType.SC_JSON) || contentType.contains(HttpContentType.SC_XML)) {
response = httpRequest.doPost(requestDO.getUrl(), requestBody);
} else if (contentType.contains(HttpContentType.SC_FROM_DATA)) {
throw new TestInterfaceException("Missing parameter, the fileParam is null!");
} else {
if (null != paramsMap) {
response = httpRequest.doPost(requestDO.getUrl(), paramsMap);
} else {
response = httpRequest.doPost(requestDO.getUrl());
}
}
} else if (requestDO.getMethod().equalsIgnoreCase(HttpMethod.Get.toString())) {
if (contentType.contains(HttpContentType.SC_JSON) || contentType.contains(HttpContentType.SC_XML)) {
response = httpRequest.doGet(requestDO.getUrl(), requestBody);
} else {
if (null != paramsMap) {
response = httpRequest.doGet(requestDO.getUrl(), paramsMap);
} else {
response = httpRequest.doGet(requestDO.getUrl());
}
}
} else if (requestDO.getMethod().equalsIgnoreCase(HttpMethod.Put.toString())) {
if (contentType.contains(HttpContentType.SC_JSON) || contentType.contains(HttpContentType.SC_XML)) {
response = httpRequest.doPut(requestDO.getUrl(), requestBody);
} else {
response = httpRequest.doPut(requestDO.getUrl(), paramsMap);
}
} else if (requestDO.getMethod().equalsIgnoreCase(HttpMethod.Delete.toString())) {
response = httpRequest.doDelete(requestDO.getUrl(), requestBody);
} else {
throw new TestInterfaceException("Missing parameter, method is " + requestDO.getMethod());
}
Log.logInfo("step: " + requestDO.getStep());
// Log.logInfo("url: " + requestDO.getUrl());
Log.logInfo("method: " + requestDO.getMethod());
// Log.logInfo("param: " + requestDO.getParam());
Log.logInfo("header: " + header);
Log.logInfo("shouldBeContains: " + requestDO.getShouldBeContains());
Log.logInfo("shouldBeNotContains: " + requestDO.getShouldBeNotContains());
Log.logInfo("shouldBeEquals: " + requestDO.getShouldBeEquals());
Log.logInfo("shouldBeContainsByRegex: " + requestDO.getShouldBeContainsByRegex());
Log.logInfo("shouldBeNotContainsByRegex: " + requestDO.getShouldBeNotContainsByRegex());
Log.logInfo("shouldBeEqualsByRegex: " + requestDO.getShouldBeEqualsByRegex());
Log.logInfo("regex: " + requestDO.getRegex());
Log.logInfo("fileParam: " + requestDO.getFileParam());
Log.logInfo("encode: " + requestDO.getEncode());
Log.logInfo("proxy: " + requestDO.getProxy());
if (null != resultDir) {
if (!FileUtil.isExists(resultDir)) {
dir = new File(resultDir);
FileUtil.makeDir(dir);
dir = null;
}
FileUtil.writeAll(resultDir + File.separator + desc, response);
}
Log.logInfo("response:" + response);
HttpTester.startTest()
.setParameters(globalData)
.sendRequest(response)
.getDynamicParamByRegx(requestDO.getRegex())
.shouldBeContains(requestDO.getShouldBeContains())
.shouldBeNotContains(requestDO.getShouldBeNotContains())
.shouldBeEquals(requestDO.getShouldBeEquals())
.shouldBeContainsByRegex(requestDO.getShouldBeContainsByRegex())
.shouldBeNotContainsByRegex(requestDO.getShouldBeNotContainsByRegex())
.shouldBeEqualsByRegex(requestDO.getShouldBeEqualsByRegex())
.setDelay(requestDO.getDelay())
.endTest();
Log.logInfo("##########################################################################################################################################################################################################");
}
httpRequest.destroy();
}
public void work(String testCase, String resultDir) {
try {
String caseName = testCase.substring(testCase.lastIndexOf(File.separator));
caseName = caseName.substring(1, caseName.indexOf("."));
init(testCase);
if (null != iterativeParamList) {
for (Map<String, String> iterativeParam : iterativeParamList) {
run(resultDir + File.separator + caseName);
}
} else {
run(resultDir + File.separator + caseName);
}
} catch (JsonValidException e) {
e.printStackTrace();
} catch (TestCaseParseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (DataProcessException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (TestInterfaceException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
完整案例之代码实现:如果要写代码完成测试案例,DSL 的设计让开发者事半功倍,让看代码的人通俗易懂
public class Demo1 {
public static void main(String[] args) {
try {
// 参数工厂,用户可定制化实现
Map<String, String> header = DataProcess.header("Content-Type::application/x-www-form-urlencoded,,accept-encoding::gzip, deflate, sdch");
Map<String, String> needReplaceParams = ParametersFactory.wantTo()
.add("user_phone", "13012345678")
.add("path", "test")
.create();
// 全局配置,配置参数都有默认值,set方法非必配
HttpConfig httpConfig = HttpConfig.custom()
.setDefaultEncode("UTF-8")
.setHeader(header)
.setRedirectStrategy(true)
.setHttps(true)
.setProxy("xx.xx.xx.xx:6666")
.build();
// 动态代理实现请求参数化,代价是牺牲DSL语义。IHttpRequest接口,用户可定制化实现
IHttpRequest httpRequest = (IHttpRequest) ParamsProxy.newInstance(new HttpRequest(httpConfig), needReplaceParams);
// 局部配置,优先级大于全局配置,配置参数都有默认值,此处代码非必配
httpRequest.setProxy("");
httpRequest.setHeader(header);
httpRequest.setDefaultEncode("");
// 测试核心模块,sendRequest必须配置,其他可选配
HttpTester.startTest()
.setParameters(needReplaceParams)
.sendRequest(httpRequest.doGet("http://xx.xx.xx.xx/${path}/test.do", "user_phone=${user_phone}&date=#{pers.quq.layer.tools.DateUtil.test}"))
.getDynamicParamByRegx("msg={msg\":\"(.*)\",\"result}#1")
.shouldBeContains("1001")
.shouldBeEquals("{\"code\":\"1001\",\"msg\":\"${msg}\",\"result\":\"\"}")
.shouldBeContainsByRegex("msg\":\".*\",\"result")
.shouldBeEqualsByRegex("{\"code\":\".*\",\"msg\":\".*\",\"result\":\"\"}")
.setDelay(3)
.endTest();
} catch (DataProcessException e) {
e.printStackTrace();
}
}
}
作为一位过来人也是希望大家少走一些弯路
在这里我给大家分享一些自动化测试前进之路的必须品,希望能对你带来帮助。
(软件测试相关资料,自动化测试相关资料,技术问题答疑等等)
相信能使你更好的进步!
点击下方小卡片