思路梳理

  • 使用 java 类来梳理 case
  • 使用 excel + pojo 来抽取封装用例数据
    • 对excel 与 pojo类进行设计以 适应不同协议的接口,携带请求头信息 等
  • 使用 testng aseert 封装的工具 来进行测试断言
  • 使用 基于httpClient 封装的工具 来进行发送请求
  • 使用 mvn 整合 allurereport 完成测试结果展示
  • 使用 moco 来对一些未完成的接口进行 mock 减少测试工程整体依赖,实现组间并行作业。

    项目整体工程目录

    image.png

    数据驱动实现

  • 因为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信息大致如下,本次案例学习研究性质更重一些,仅提供思路参考
- ![image.png](https://cdn.nlark.com/yuque/0/2021/png/1608527/1636976470634-5574ef1f-da55-4cc5-b0f8-b13caa5bc2d0.png#clientId=ucc6f8256-235d-4&from=paste&height=247&id=u2d671c7c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=494&originWidth=2750&originalType=binary&ratio=1&size=350343&status=done&style=none&taskId=u3caacda3-3543-4f7e-b18b-c20fca63ca0&width=1375)
<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以及拼接规则等等

![image.png](https://cdn.nlark.com/yuque/0/2021/png/1608527/1636976866089-115a6e88-1b6b-4aa5-8ff6-4ba5bf46cb18.png#clientId=ucc6f8256-235d-4&from=paste&id=u324ad80c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=432&originWidth=736&originalType=binary&ratio=1&size=288482&status=done&style=none&taskId=u756eb423-8527-427e-9676-37ae54d4c58)
```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 {
       //将主体信息对象转为字符串
       responseEntity = EntityUtils.toString(entity, "UTF-8");
      
      } catch (ParseException e) {
       Log.error("+++++响应主体信息转换异常+++++");
      
      } catch (IOException e) {
       Log.error("+++++响应主体信息IO异常+++++");
      
      } return responseEntity; }
/**
 * 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

代码地址

  • 当前为一个粗略的版本,不排除后续会对其进行迭代加强。

https://gitee.com/shihu1/Automation_Api