基於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("\r\n", "").replace("\n", ""));
		//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


免責聲明!

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



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