基於Java+HttpClient+TestNG的接口自動化測試框架(十一)------ 確定接口請求數據流程並執行測試


  前面的部分,我們主要是對各種部分的數據處理進行說明。本篇來說明一下,接口請求數據的流程及一些問題的處理。

  我們知道,在進行接口測試之前,通常會對環境進行一些配置。比如:host的設定,一些固定參數的設定等等。關於環境設定的方面,我們通常還是通過xml的方式來進行設定。請參考之前的有關參數設定的章節。基於Java+HttpClient+TestNG的接口自動化測試框架(二)------配置文件的設定及讀取

  在進行環境設定之后,我們需要使用TestNG來制作一個接口請求的模板類,具體來說就是所有類型的接口都可以按照這個模板來進行請求,並判定結果是否正確。

  在通常來說,接口的工作流程,可以參考下面的形式:

處理環境參數-------->處理請求參數-------->封裝請求對象------->運行請求------->分析請求返回結果並判定------->生成log和報告------>對返回結果進行提取保存。

       根據以上的流程,我們在作成這個TestNG的類:

 

package testSysApi;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import bean.ApiDataBean;
import configs.apiConfigs;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.dom4j.DocumentException;
import org.testng.Assert;
import org.testng.ITestContext;
import org.testng.annotations.*;
import org.testng.annotations.Optional;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Matcher;

import listener.AutoTestListener;
import listener.RetryListener;
import testCase.TestBase;
import utils.fileUtil;
import utils.randomUtil;
import utils.reportUtil;

@Listeners({ AutoTestListener.class, RetryListener.class })
public class customTest extends TestBase {

    /**
     * api請求跟路徑
     */
    private static String rootUrl;

    /**
     * 跟路徑是否以‘/’結尾
     */
    private static boolean rooUrlEndWithSlash = false;

    /**
     * 所有公共header,會在發送請求的時候添加到http header上
     */
    private static Header[] publicHeaders;
    /**
     * 是否使用form-data傳參 會在post與put方法封裝請求參數用到
     */
    private static boolean requestByFormData = false;

    /**
     * 配置
     */
    private static apiConfigs apiConfig;

    /**
     * 所有api測試用例數據
     */
    protected List<ApiDataBean> dataList = new ArrayList<ApiDataBean>();
    private static HttpEntity httpEntity;
    private static HttpClient client;

    /**
     * 初始化測試數據
     *
     * @throws Exception
     */
    @Parameters("envName")
    @BeforeSuite
    public void init(@Optional("config.xml") String envName) throws Exception {
        String configFilePath = Paths.get(System.getProperty("user.dir")+"\\config\\", envName).toString();
        reportUtil.log("api config path:" + configFilePath);
        apiConfig = new apiConfigs(configFilePath);
        // 獲取基礎數據
        rootUrl = apiConfig.getRootUrl();
        rooUrlEndWithSlash = rootUrl.endsWith("/");

        // 讀取 param,並將值保存到公共數據map
        Map<String, String> params = apiConfig.getParams();
        setSaveDatas(params);
        
        //讀取配置xml文件,將公共請求頭進行設置
        List<Header> headers = new ArrayList<Header>();
        apiConfig.getHeaders().forEach((key, value) -> {
            Header header = new BasicHeader(key, value);
            if(!requestByFormData && key.equalsIgnoreCase("content-type") && value.toLowerCase().contains("form-data")){
                requestByFormData=true;
            }
            headers.add(header);
        });
        publicHeaders = headers.toArray(new Header[headers.size()]);
        //對HttpClient設置超時時間
        RequestConfig reqCon = RequestConfig.custom()
                .setConnectTimeout(60000)
                .setConnectionRequestTimeout(60000)
                .setSocketTimeout(60000).build();
        client = HttpClients.custom().setDefaultRequestConfig(reqCon).build();
    }

    @Parameters({ "excelPath"})
    @BeforeTest
    public void readData(@Optional("case/test-data.xls") String excelPath,ITestContext context) throws DocumentException {
        //獲取xml中所有的參數
        Map<String,String> xmlParam = context.getCurrentXmlTest().getAllParameters();
        List<String> sheetsName = new ArrayList<String>();
        /*
         * 可以指定多個sheetName的名字來進行測試
         * 形式如        <parameter name="sheetName1" value="User"></parameter>
                            <parameter name="sheetName2" value="Product"></parameter>
         *  這里如果不進行過濾,可以修改為默認進行所有sheet的測試
         */
        for(String s : xmlParam.keySet()) {
            if(s.contains("sheetName")) {
                sheetsName.add(xmlParam.get(s));
            }
        }
        String[] sheets = sheetsName.toArray(new String[sheetsName.size()]);
        dataList = readExcelData(ApiDataBean.class, excelPath.split(";"),sheets);
    }

    /**
     * 過濾數據,run標記為Y的執行。
     *
     * @return
     * @throws DocumentException
     */
    @DataProvider(name = "apiDatas")
    public Iterator<Object[]> getApiData(ITestContext context)
            throws DocumentException {
        List<Object[]> dataProvider = new ArrayList<Object[]>();
        for (ApiDataBean data : dataList) {
            if (data.isRun()) {
                dataProvider.add(new Object[] { data });
            }
        }
        return dataProvider.iterator();
    }

    @Test(dataProvider = "apiDatas")
    public void apiTest(ApiDataBean apiDataBean) throws Exception {
        reportUtil.log("------------------test start --------------------");
        if (apiDataBean.getSleep() > 0) {
            // sleep休眠時間大於0的情況下進行暫停休眠
            reportUtil.log(String.format("sleep %s seconds",
                    apiDataBean.getSleep()));
            Thread.sleep(apiDataBean.getSleep() * 1000);
        }
        String apiParam = buildRequestParam(apiDataBean);
        //由於headers要入參,因此Excel的
        String headers = buildRequestHeader(apiDataBean);
        // 封裝請求方法
        HttpUriRequest method = parseHttpRequest(apiDataBean.getUrl(),
                apiDataBean.getMethod(),headers,apiParam);
        String responseData;
        try {
            //增加運行時間計算顯示
            LocalDateTime beginTime = LocalDateTime.now();
            // 執行
            HttpResponse response = client.execute(method);
            Long timeConsuming = Duration.between(beginTime,LocalDateTime.now()).toMillis();
            reportUtil.log("測試執行時間為:" + timeConsuming + "ms!"); 
            int responseStatus = response.getStatusLine().getStatusCode();
            reportUtil.log("返回狀態碼:"+responseStatus);
            if (apiDataBean.getStatus()!= 0) {
                Assert.assertEquals(responseStatus, apiDataBean.getStatus(),
                        "返回狀態碼與預期不符合!");
            } 
            else {
                // 非2開頭狀態碼為認為是異常請求,拋出異常
                if (200 > responseStatus || responseStatus >= 300) {
                    reportUtil.log("返回狀態碼非200開頭:"+EntityUtils.toString(response.getEntity(), "UTF-8"));
                    throw new Exception("返回狀態碼異常:"+ responseStatus);
                }
            }
            HttpEntity respEntity = response.getEntity();
            Header respContentType = response.getFirstHeader("Content-Type");
            if (respContentType != null && respContentType.getValue() != null 
                    &&  (respContentType.getValue().contains("download") || respContentType.getValue().contains("octet-stream"))) {
                String conDisposition = response.getFirstHeader(
                        "Content-disposition").getValue();
                String fileType = conDisposition.substring(
                        conDisposition.lastIndexOf("."),
                        conDisposition.length());
                String filePath = "download/" + randomUtil.getRandom(8, false)
                        + fileType;
                InputStream is = response.getEntity().getContent();
                Assert.assertTrue(fileUtil.writeFile(is, filePath), "下載文件失敗。");
                // 將下載文件的路徑放到{"filePath":"xxxxx"}進行返回
                responseData = "{\"filePath\":\"" + filePath + "\"}";
            } else {
                responseData=EntityUtils.toString(respEntity, "UTF-8");
            }
        } catch (Exception e) {
            throw e;
        } finally {
            method.abort();
        }
        // 輸出返回數據log
        reportUtil.log("resp:" + responseData);
        // 驗證預期信息
        verifyResult(responseData, apiDataBean.getVerify(),
                apiDataBean.isContains());

        // 對返回結果進行提取保存。
        saveResult(responseData, apiDataBean.getSave());
    }

    private String buildRequestParam(ApiDataBean apiDataBean) {
        // 分析處理預參數 (函數生成的參數)
        String preParam = buildParam(apiDataBean.getPreParam());
        savePreParam(preParam);// 保存預存參數 用於后面接口參數中使用和接口返回驗證中
        // 處理參數
        String apiParam = buildParam(apiDataBean.getParam());
//        System.out.println(apiParam);
        return apiParam;
    }
    /*
     * 獲取Excel文件中設置的關鍵性Header信息,例如:Content-Type,Authorization等
     */
    private String buildRequestHeader(ApiDataBean apiDataBean) {
        String header = "";
        header = buildParam(apiDataBean.getHeader());
        return header;
    }
    /*
     * 這里的header是Excel中設置的json字符串
     */
    private HttpUriRequest parseHttpRequest(String url, String method,String header,String param) throws ParseException, IOException {
        Map<String,String> publicMaps = new HashMap<String,String>();
        for(Header he : publicHeaders) {
            publicMaps.put(he.getName(), he.getValue());
        }
        // 處理url
        url = parseUrl(url);
        reportUtil.log("method:" + method);
        reportUtil.log("url:" + url);
        reportUtil.log("publicHeaders:" + JSONObject.toJSONString(publicMaps));
        reportUtil.log("header:" + header);
        reportUtil.log("param:" + param.replace("\r\n", "").replace("\n", ""));
        if(header != null) {
            @SuppressWarnings("unchecked")
            Map<String,String> headers = JSON.parseObject(header,HashMap.class);
            publicMaps.putAll(headers);
        }
        //使用Content-Type的值來判定具體body上傳模式
        List<String> values = new ArrayList<String>();
        for(String s : publicMaps.keySet()) {
            values.add(publicMaps.get(s));
        }
        System.out.println(values);
        //upload表示上傳,也是使用post進行請求
        if ("post".equalsIgnoreCase(method) || "upload".equalsIgnoreCase(method)) {
            // 封裝post方法
            HttpPost postMethod = new HttpPost(url);
            Set<Map.Entry<String, String>> set = publicMaps.entrySet();
            Iterator<Map.Entry<String, String>> it = set.iterator();
            while(it.hasNext()) {
                Map.Entry<String, String> entry = it.next();
                //如果遇到"Content-Type:multipart/form-data"的情況,請不要加入該請求頭。
                //通過抓包可以發現,
                //一般Content-Type:multipart/form-data 后面會加上一串 boundary=--------------------------016172816456888939258535的信息
                //這個信息是動態變化的。
                if(entry.getValue().equals("multipart/form-data")) {
                    continue;
                }else {
                    postMethod.addHeader(entry.getKey(),entry.getValue());
                }
            }    
            //根據請求頭的content-type的值,來分別選擇上傳形式
            HttpEntity entity  = parseEntity(param,values);
            postMethod.setEntity(entity);
            return postMethod;
        } else if ("put".equalsIgnoreCase(method)) {
            // 封裝put方法
            HttpPut putMethod = new HttpPut(url);
            Set<Map.Entry<String, String>> set = publicMaps.entrySet();
            Iterator<Map.Entry<String, String>> it = set.iterator();
            while(it.hasNext()) {
                Map.Entry<String, String> entry = it.next();
                putMethod.addHeader(entry.getKey(),entry.getValue());
            }
            HttpEntity entity  = parseEntity(param,values);
            putMethod.setEntity(entity);
            return putMethod;
        } else if ("delete".equalsIgnoreCase(method)) {
            // 封裝delete方法
            HttpDelete deleteMethod = new HttpDelete(url);
            deleteMethod.setHeaders(publicHeaders);
            return deleteMethod;
        }else {
            // 封裝get方法
            HttpGet getMethod = new HttpGet(url);
            Set<Map.Entry<String, String>> set = publicMaps.entrySet();
            Iterator<Map.Entry<String, String>> it = set.iterator();
            while(it.hasNext()) {
                Map.Entry<String, String> entry = it.next();
                getMethod.addHeader(entry.getKey(),entry.getValue());
            }
            return getMethod;    
        }
    }

    /**
     * 格式化url,替換路徑參數等。
     *
     * @param shortUrl
     * @return
     */
    private String parseUrl(String shortUrl) {
        // 替換url中的參數
        shortUrl = getCommonParam(shortUrl);
        if (shortUrl.startsWith("http")) {
            return shortUrl;
        }
        if (rooUrlEndWithSlash == shortUrl.startsWith("/")) {
            if (rooUrlEndWithSlash) {
                shortUrl = shortUrl.replaceFirst("/", "");
            } else {
                shortUrl = "/" + shortUrl;
            }
        }
        return rootUrl + shortUrl;
    }

    /**
     * 格式化參數,根據請求頭的形式來決定如何封裝entity,這里主要列了三種形式。
     * @param param 參數
     * @param headerValueList 請求頭列表里的數據。根據請求頭的數據來決定封裝形式。
     * @return
     * @throws IOException 
     * @throws ParseException 
     */
    @SuppressWarnings("unchecked")
    private HttpEntity parseEntity(String param,List<String> headerValueList) throws ParseException, IOException{
        int requestBodyNum = 0;
        for(String headerValue : headerValueList) {
            if(headerValue.contains("multipart/form-data")) {
                requestBodyNum = 1;
            }else if(headerValue.contains("application/x-www-form-urlencoded")) {
                requestBodyNum = 2;
            }else if(headerValue.equalsIgnoreCase("application/json")) {
                requestBodyNum = 3;
            }
        }
        switch (requestBodyNum) {
        case 1:
            Map<String, String> paramMap = JSON.parseObject(param,HashMap.class);
            Charset charset = Charset.defaultCharset();
            MultipartEntityBuilder builder = MultipartEntityBuilder.create().setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
                    .setCharset(charset);
            for (String key : paramMap.keySet()) {
                String value = paramMap.get(key);
                Matcher m = funPattern.matcher(value);
                if (m.matches() && m.group(1).equals("bodyfile")) {
                    value = m.group(2);
                    builder.addPart(key, new FileBody(new File(value)));
                } else {
                    StringBody stringBody = new StringBody(null == value ? "" : value.toString()
                            , ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), charset)); //編碼
                    builder.addPart(key, stringBody);
                }
            }
            httpEntity = builder.build();
            break;
        case 2:
            Map<String, String> bodyMaps = JSON.parseObject(param,HashMap.class);
            List<NameValuePair> bodyParams = new ArrayList<NameValuePair>();
            for(String key: bodyMaps.keySet()) {
                String value = bodyMaps.get(key);
                bodyParams.add(new BasicNameValuePair(key,value));
            }
            httpEntity = new UrlEncodedFormEntity(bodyParams,"UTF-8");
            break;
        case 3 :
            httpEntity = new StringEntity(param,"UTF-8");
            break;
        }
        return httpEntity;
    }
}

  從上面的模板代碼,我們完成了整個接口請求的流程。在實際的操作中,我們只需要指定case文件(Excel)和配置文件(xml),就可以對接口進行自動化測試了。當然,運行TestNG,我們也是采用xml的運行方式,這個的寫法就很簡單了。指明需要運行的類和方法,並配置好監聽器用來生成報告即可,下面給出一個模板。

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="SYS接口自動化測試" verbose="1" preserve-order="true" parallel="false">
    <test name="SYS自動化測試用例">
        <parameter name="excelPath" value="case/test-data.xls"></parameter>
        <parameter name="sheetName1" value="User"></parameter>
        <parameter name="sheetName2" value="Product"></parameter>
        <classes>
            <class name="testSysApi.customTest">
                <methods>                            
                    <include name="apiTest"></include>
                </methods>
            </class>    
        </classes>
    </test>
    <listeners>    
        <listener class-name="listener.AutoTestListener"></listener>
        <listener class-name="listener.RetryListener"></listener>
        <!-- ExtentReport 報告  -->
        <listener class-name="listener.ExtentTestNGIReporterListener"></listener>
    </listeners>
</suite> 

  整體來說,這就完成了接口自動化測試的一個小框架。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM