分层测试设计之接口层

转载地址:http://blog.csdn.net/testman930/article/details/50792071

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

接口层优化需求

总体目标

  • 支持基于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。。。。。。
普通的测试案例:如果不想写代码,就这么简单
{
    "testcase": "layTestCase",
    "scope": 1,
    "useHttps": true,
    "redirect": true,
    "defaultEncode": "UTF-8",
    "requests": [
        {
            "step": 1,
            "url": "http://xx.xx.xx.xx/app/test.do",
            "method": "get",
            "param": "user_phone=13012345678&date=20160303",
            "shouldBeContainsByRegex": "msg\":\".*\",\"result",
        }
    ]
}

参数化处理的测试案例:参数化功能强大,配置相对简单
{
    "testcase": "layTestCase",
    "scope": 1,
    "useHttps": true,
    "redirect": true,
    "globalData": "userLogin=UserLogin,rsp=Rsp,name=qq",
    "iterativeData": "outer file=src/parameters&fields=phone,clientType&separator=:",
    "defaultEncode": "UTF-8",
    "defaultHeader": "Content-Type::application/x-www-form-urlencoded,,accept-encoding::gzip, deflate, sdch",
    "requests": [
        {
            "step": 1,
            "url": "https://test-demo.${name}.com/testPost.do",
            "method": "post",
            "param": "aaa=<clientType=\"${clientType}\" osType=\"android4.4.4\" phone=\"${phone}\"",
            "encode": "UTF-8",
        "shouldBeContains": "phone=\"${phone}\"",
        },
        {
            "step": 2,
            "url": "http://xx.xx.xx.xx/app/testGet.do",
            "method": "get",
            "param": "user_phone=${user_phone}&date=#{pers.quq.layer.tools.DateUtil.test}",
            "shouldBeContains": "1001",
            "shouldBeContainsByRegex": "msg\":\".*\",\"result",
        }
    ]
}

经过本次代码重构,以上原本复杂的,对外部文件解析执行的核心处理类,逻辑变得非常简单

/**
 * 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;

       // 全局性配置
        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() throws DataProcessException, UnsupportedEncodingException, URISyntaxException, TestInterfaceException {
        for (RequestDO requestDO : requests) {
            // 局部配置
            header = null != requestDO.getHeader() ? DataProcess.header(requestDO.getHeader()) : null;
            contentType = null != requestDO.getContentType() ? requestDO.getContentType().trim() : HttpContentType.SC_FROM_URL_ENCODED;
            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.SC_POST)) {
                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.SC_GET)) {
                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.doPost(requestDO.getUrl());
                    }
                }
            } else {
                throw new TestInterfaceException("Missing parameter, method is " + requestDO.getMethod());
            }

            // 执行测试
            HttpTester.startTest()
                    .setParameters(globalData)
                    .sendRequest(response)
                    .getDynamicParamByRegx(requestDO.getRegex())
                    .shouldBeContains(requestDO.getShouldBeContains())
                    .shouldBeEquals(requestDO.getShouldBeEquals())
                    .shouldBeContainsByRegex(requestDO.getShouldBeContainsByRegex())
                    .shouldBeEqualsByRegex(requestDO.getShouldBeEqualsByRegex())
                    .setDelay(requestDO.getDelay())
                    .endTest();
        }

        // 销毁对象
        httpRequest.destroy();
    }

    public void work(String testCase) {
        // 外壳控制逻辑
        try {
            init(testCase);
            if (null != iterativeParamList) {
                for (Map<String, String> iterativeParam : iterativeParamList) {
                    run();
                }
            } else {
                run();
            }
        } catch (JsonValidException e) {
            e.printStackTrace();
        } catch (TestCaseParseException e) {
            e.printStackTrace();
        } catch (IOException 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();
        }
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值