zoukankan      html  css  js  c++  java
  • 基于maven+java+TestNG+httpclient+poi+jsonpath+ExtentReport的接口自动化测试框架

    接口自动化框架

    项目说明

    • 本框架是一套基于maven+java+TestNG+httpclient+poi+jsonpath+ExtentReport而设计的数据驱动接口自动化测试框架,TestNG 作为执行器,poi用于读取存放于excel的接口用例,jsonPath用于校验返回值,以及提取返回值。本框架无需你使用代码编写用例,在excel中即可进行接口用例编写,接口依赖关联,接口断言,控制用例的运行。

    技术栈

    • maven
    • java
    • TestNG
    • httpclient
    • poi
    • jsonpath
    • ExtentReport

    环境部署

    • 安装jdk8,并配置好环境变量
    • maven中直接导入项目工程包,导入成功后,maven会自动下载当前项目的所有依赖包

    代码设计与功能说明

    1、定义运行配置文件 api-config.xml

    api请求根路径、请求头及初始化参数值可以在api-config上进行配置。

    • rootUrl: 必须的配置,api的根路径,在调用api时用于拼接,配置后,会在自动添加到用例中的url的前缀中。
    • headers: 非必须配置,配置后在调用api时会将对应的name:value值设置到所有请求的请求头中header-name:header-value。
    • params:非必须配置,公共参数,通常放置初始化配置数据,所有用例执行前,会将params下所有的param配置进行读取并存储到公共参数池中,在用例执行时,使用特定的关键字(${param_name})可以获取。具体如下:

    api-config.xml配置信息
    
    <?xml version="1.0" encoding="UTF-8"?>
    <root>
        <rootUrl>http://127.0.0.1:12306</rootUrl>
        <headers>
            <!-- 配置为自己的参数 -->
            <header name="Content-Type" value="application/json;charset=UTF-8"></header>
        </headers>
        <params>
        	<param name="" value=""></param>
        </params>
        <project_name>接口自动化测试报告demo</project_name>
    </root>
    

    2、测试用例的设计

    测试用例以excel格式的文件保存,除表头外,一行代表一个api用例。执行时会依次从左到右,从上到下执行。case/api-data.xls测试用例的数据格式如下:

    • run:标记为‘Y’时,该行数据会被读取执行;标记为‘N’则不被执行
    • description:该用例描述,在报告中体现。
    • method:该api测试用例的请求方法。
    • url:该api测试用例的请求路径。
    • 说明:
    • param:请求方法为post时,body的内容(暂只支持json,不支持xml)
    • verify:对于api请求response数据的验证(可使用jsonPath进行校验)。校验多个使用“;”进行隔开。
    • 若verify填写值为:$.username=wuya;$.userID=22 ,则会校验返回值中$.username的值为wuya,$.userID的值为22,只要有一个校验错误,后面的其他校验项将停止校验。
    • save:使用jsonPath对response的数据进行提取存储。
    • 说明:若save值为:id=$.userId;age=$.age ,接口实际返回内容为:{"username":"chenwx","userId":"1000","age":"18"},则接口执行完成后,会将公共参数userId的值存储为1000,age存储为18。公共参数可在后面的用例中进行使用。
    • 公共关联池中的公共参数使用
    • 测试用例excel表中可以使用‘${param_name}’占位符,在执行过程中如果判断含有占位符,则会将该值替换为公共参数里面的值,如果找不到将会报错。具体使用格式如下:

    {
    "token":"${g_token}",
    "vpl":"AJ3585"
    }
    

    3、函数助手

    测试用例excel表中可以使用‘__funcName(args)’占位符,在执行过程中如果判断含有该占位符,且funcName存在,则会执行相应的函数后进行替换。部分函数说明如下:

    • __random(param1,param2):随机生成一个定长的字符串(不含中文)。param1:长度(非必填,默认为6),param2:纯数字标识(非必填,默认为false)。
    • __randomText(param1): 随机生成一个定长的字符串(含中文)。param1:长度(非必填,默认为6)
    • __date(param1): 生成执行该函数时的时间格式化字符串。param1为转换的格式,默认为生成当前13位时间戳。
    • 具体使用格式如下:

    {
    "drivers":"张三",
    "cmsuer":"__random(8,false)",
    "time":"__date()"
    }
    

    函数random执行时会产生8位长度的随机字符串,并传给变量cmsuer;函数date在执行时,会产生一个13位的时间戳,并传给变量time。

    4、测试执行主程序


    package test.com.sen.api;
    
    import com.alibaba.fastjson.JSON;
    import com.sen.api.beans.ApiDataBean;
    import com.sen.api.configs.ApiConfig;
    import com.sen.api.excepions.ErrorRespStatusException;
    import com.sen.api.listeners.AutoTestListener;
    import com.sen.api.listeners.RetryListener;
    import com.sen.api.utils.*;
    import org.apache.http.Header;
    import org.apache.http.HttpEntity;
    import org.apache.http.HttpResponse;
    import org.apache.http.client.HttpClient;
    import org.apache.http.client.methods.*;
    import org.apache.http.entity.StringEntity;
    import org.apache.http.entity.mime.MultipartEntity;
    import org.apache.http.entity.mime.content.FileBody;
    import org.apache.http.entity.mime.content.StringBody;
    import org.apache.http.message.BasicHeader;
    import org.apache.http.params.CoreConnectionPNames;
    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.InputStream;
    import java.io.UnsupportedEncodingException;
    import java.nio.file.Paths;
    import java.util.*;
    import java.util.regex.Matcher;
    
    @Listeners({ AutoTestListener.class, RetryListener.class })
    public class ApiTest 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 ApiConfig apiConfig;
    
    	/**
    	 * 所有api测试用例数据
    	 */
    	protected List<ApiDataBean> dataList = new ArrayList<ApiDataBean>();
    
    	private static HttpClient client;
    
    	/**
    	 * 初始化测试数据
    	 *
    	 * @throws Exception
    	 */
    	@Parameters("envName")
    	@BeforeSuite
    	public void init(@Optional("api-config.xml") String envName) throws Exception {
    		String configFilePath = Paths.get(System.getProperty("user.dir"), envName).toString();
    		ReportUtil.log("api config path:" + configFilePath);
    		apiConfig = new ApiConfig(configFilePath);
    		// 获取基础数据
    		rootUrl = apiConfig.getRootUrl();
    		rooUrlEndWithSlash = rootUrl.endsWith("/");
    
    		Map<String, String> params = apiConfig.getParams();
    		setSaveDates(params);
    
    		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()]);
    		client = new SSLClient();
    		client.getParams().setParameter(
    				CoreConnectionPNames.CONNECTION_TIMEOUT, 60000); // 请求超时
    		client.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 60000); // 读取超时
    	}
    
    	@Parameters({ "excelPath", "sheetName" })
    	@BeforeTest
    	public void readData(@Optional("case/api-data.xls") String excelPath, @Optional("Sheet1") String sheetName) throws DocumentException {
    		dataList = readExcelData(ApiDataBean.class, excelPath.split(";"),
    				sheetName.split(";"));
    	}
    
    	/**
    	 * 过滤数据,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) {
    //			poi解析处理Excel表时,若单元格中的布尔值为Y或者true,则解析到的布尔值为true;若单元格中的布尔值为false或者其他值或者为空,则解析到的布尔值为false
    			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);
    		// 封装请求方法
    		HttpUriRequest method = parseHttpRequest(apiDataBean.getUrl(),
    				apiDataBean.getMethod(), apiParam);
    		String responseData;
    		try {
    			// 执行
    			HttpResponse response = client.execute(method);
    			int responseStatus = response.getStatusLine().getStatusCode();
    			ReportUtil.log("返回状态码:"+responseStatus);
    			if (apiDataBean.getStatus()!= 0) {
    				Assert.assertEquals(responseStatus, apiDataBean.getStatus(),
    						"返回状态码与预期不符合!");
    			} 
    
    			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());
    		return apiParam;
    	}
    
    	/**
    	 * 封装请求方法
    	 *
    	 * @param url
    	 *            请求路径
    	 * @param method
    	 *            请求方法
    	 * @param param
    	 *            请求参数
    	 * @return 请求方法
    	 * @throws UnsupportedEncodingException
    	 */
    	private HttpUriRequest parseHttpRequest(String url, String method, String param) throws UnsupportedEncodingException {
    		// 处理url
    		url = parseUrl(url);
    		ReportUtil.log("method:" + method);
    		ReportUtil.log("url:" + url);
    		ReportUtil.log("param:" + param.replace("
    ", "").replace("
    ", ""));
    		//upload表示上传,也是使用post进行请求
    		if ("post".equalsIgnoreCase(method) || "upload".equalsIgnoreCase(method)) {
    			// 封装post方法
    			HttpPost postMethod = new HttpPost(url);
    			postMethod.setHeaders(publicHeaders);
    			//如果请求头的content-type的值包含form-data 或者 请求方法为upload(上传)时采用MultipartEntity形式
    			HttpEntity entity  = parseEntity(param,requestByFormData || "upload".equalsIgnoreCase(method));
    			postMethod.setEntity(entity);
    			return postMethod;
    		} else if ("put".equalsIgnoreCase(method)) {
    			// 封装put方法
    			HttpPut putMethod = new HttpPut(url);
    			putMethod.setHeaders(publicHeaders);
    			HttpEntity entity  = parseEntity(param,requestByFormData );
    			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);
    			getMethod.setHeaders(publicHeaders);
    			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;
    	}
    
    	/**
    	 * 格式化参数,如果是from-data格式则将参数封装到MultipartEntity否则封装到StringEntity
    	 * @param param 参数
    	 * @param formData 是否使用form-data格式
    	 * @return
    	 * @throws UnsupportedEncodingException
    	 */
    	private HttpEntity parseEntity(String param,boolean formData) throws UnsupportedEncodingException{
    		if(formData){
    			Map<String, String> paramMap = JSON.parseObject(param,
    					HashMap.class);
    			MultipartEntity multiEntity = new MultipartEntity();
    			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);
    					multiEntity.addPart(key, new FileBody(new File(value)));
    				} else {
    					multiEntity.addPart(key, new StringBody(paramMap.get(key)));
    				}
    			}
    			return multiEntity;
    		}else{
    			return new StringEntity(param, "UTF-8");
    		}
    	}
    
    }
    

    5、测试总执行器testng.xml(收集测试用例,批量执行并生成测试报告)


    <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
    <suite name="接口自动化测试" verbose="1" preserve-order="true" parallel="false">
    	<test name="自动化测试用例">
    		<parameter name="excelPath" value="case/api-data.xls"></parameter>
    		<parameter name="sheetName" value="Sheet1"></parameter>
    		<classes>
    			<class name="test.com.sen.api.ApiTest">
    				<methods>							
    					<include name="apiTest"></include>
    				</methods>
    			</class>	
    		</classes>
    	</test>
    	<listeners>	
    		<listener class-name="com.sen.api.listeners.AutoTestListener"></listener>
    		<listener class-name="com.sen.api.listeners.RetryListener"></listener>
    		<!-- ExtentReport 报告  -->
    		<listener class-name="com.sen.api.listeners.ExtentTestNGIReporterListener"></listener>
    	</listeners>
    </suite> 
    

    6、测试运行方式

    1. IDEA工具直接执行testng.xml(以testng形式运行)即可(IDEA工具需要先装好testng插件)
    2. maven执行:根目录下,执行 mvn test

    7、测试报告呈现

    1. testng.xml执行可视化报告:${workspace}/test-output/index.html
    2. maven执行报告:${workspace}/target/test-output/index.html
  • 相关阅读:
    C语言常用函数
    信号处理函数误用不可重入函数导致的进程死锁情况
    Shell脚本学习
    文件I/O详解
    字体标记的使用
    无序列表标记
    换行标记的使用
    HTML文件的基本结构
    预格式化标记的使用
    特殊标记的使用
  • 原文地址:https://www.cnblogs.com/jun-zi/p/12061282.html
Copyright © 2011-2022 走看看