前面的部分,我们主要是对各种部分的数据处理进行说明。本篇来说明一下,接口请求数据的流程及一些问题的处理。
我们知道,在进行接口测试之前,通常会对环境进行一些配置。比如: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(" ", "").replace(" ", "")); 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>
整体来说,这就完成了接口自动化测试的一个小框架。