前面的部分,我們主要是對各種部分的數據處理進行說明。本篇來說明一下,接口請求數據的流程及一些問題的處理。
我們知道,在進行接口測試之前,通常會對環境進行一些配置。比如: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>
整體來說,這就完成了接口自動化測試的一個小框架。