之前说过要写一个测试框架,是对之前分层测试框架进行重构的大改版。一个月已过,接口层的基础版完成了。按优先级,下面会对数据层进行重构,我的愿景是把独立的层级测试用纽带联系在一起,它们之间既能结合也能解耦,这是今年的目标,后面再考虑做平台调底层的分层框架。
由于之前那篇帖子未收集到有价值的需求,只能根据以往接口测试的经验,自己给自己提需求,考虑不足之处还请指出。同时,希望能对看过的人有所帮助。

接口层优化需求

总体目标

实现功能

具体实现

如何通过文件写一个接口测试
设计采用 json 格式,一方面是方便对结构的正确性进行校验,另一方面考虑到大部分数据传输都用 json 格式,便于推广。

配置字段说明:
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();
        }
    }
}


↙↙↙阅读原文可查看相关链接,并与作者交流