介面自動化測試的一些思考和我自己的一整套處理方案 (java 向)

luoy發表於2020-12-16

介面自動化測試的一些思考

前言

之前社群一直討論很火的介面測試框架實現,到底是高大上的傻瓜式介面平臺好用,還是全指令碼的編寫的介面框架好這兩個方案其實我都有考慮過,兩個方案各有優缺點, 我個人理解就是介面平臺優點可以降低學習成本,快速使用, 全指令碼編寫介面框架的優點為靈活性高,各種疑難雜症的用例都能解決,而且擴充性好

在思考這兩條路上,我差點做了介面平臺,主要基於兩點,第一點就是我上家公司,在介面平臺還沒火起來還沒這個概念的時候,我寫的介面測試框架基本上是全程式碼實現,在後面的大面積應用過程中,我也感覺到了很多缺點,比如分層不清晰, 大量用例維護心累等問題.第二點是介面平臺能在像領導彙報時候提升逼格,出去面試時候也能吹吹

最後的結果就是何不兩種方案結合下,可以提供一個測試資料模板直接把用例編寫好,然後程式碼端又提供很好的擴充,有了這個想法就開始了我的擼碼生活

技術選型

主要考慮到一些介面需要支援的東西,然後從這方面開始選型

首先技術棧是java, 那底層支援就好選了, springBoot + maven搞起來

java測試框架嘛 testng搞起來, 支援多執行緒多緯度執行測試用例,支援重試,支援資料驅動等等等等

測試報告allure, 和testng完美相容,且有現成jenkins外掛

測試資料yaml儲存

http請求restassured 這個http請求庫基本上為測試而生

選型基本敲定,從底層到測試報告到ci/cd

版本迭代

1.最初版本 0.0.1

1.1 具體實現方案為編寫一個介面api, 該api定義baseUrl, 且通過方法與註解來描述裡面的介面,返回一個restassured 的response, 然後通過代理類解析該介面生成一個具體實現類注入spring容器中

1.2 測試用例繼承AbstractTestNGSpringContextTests, 用@SpringBootTest開啟spring容器

1.3 測試用例可用@Autowired注入介面類,然後像呼叫普通方法一樣呼叫

大體程式碼如下:

介面定義類:

@HttpServer(baseUrl = "https://testplatform.letzgo.com.cn/testool-api")
@Filters({RestAssuredLogFilter.class, RequestLoggingFilter.class, ResponseLoggingFilter.class})
public interface UserApi {

@Get(url = "/menu/list", descriptoin = "獲取所有選單")
Response getMenus();

@Get(url = "/menu/byPhone", descriptoin = "根據手機號碼查詢選單")
Response byPhone(@ParamsForm("phone") String phone);

@Get(url = "/role/list", descriptoin = "分頁查詢角色")
Response getRoleListPage(@Head("pageNo") String pageNo, @Head("pageSize") String pageSize, @ParamsForm("roleName") String roleName);

@Post(url = "/role/add", descriptoin = "增加使用者角色")
Response addRole(@ParamsJson String json);
}

測試用例類:

@Features("ces")
@Stories("測試Http")
public class TestoolApiTest extends BaseApiTestCase {
@Autowired
private UserApi userApi;

@Severity(SeverityLevel.CRITICAL)
@Description("測試api測試")
@Title("TestoolUserApi")
@Test(groups = "test-groups-1", dataProvider = "loadDataParams")
//失敗重試兩次
@RetryCount(count = 2)
@DataParams({"1,10,"})
public void getRoleListPage(String pageNo, String pageSize, String roleName) {
Response response = userApi.getRoleListPage(pageNo, pageSize, roleName);
response.then()
.statusCode(200)
.body("code", equalTo(200))
.body("success", equalTo(true));
}
}

baseTestCase主要做一些繼承和引數化

//properties屬性指定本地測試需要用到的配置檔案
//本地執行時,需要設定properties值為具體的某個配置檔案
@SpringBootTest(properties = {"spring.profiles.active=local"})
//通過maven命令執行時需要把該引數去掉
//@SpringBootTest
public abstract class BaseTestCase extends AbstractTestNGSpringContextTests {
@DataProvider
public static Object[][] loadDataParams(Method method){
DataParams dataParams = method.getAnnotation(DataParams.class);

AssertUtils.notNull(dataParams, method.getName() + "方法新增了DataProvider資料驅動,沒有新增@DataParams註解");

String[] values = dataParams.value();

String split = dataParams.splitBy();

List<Object[]> result = Lists.newArrayList();

for (int i = 0; i < values.length; i++) {
String[] v1 = values[i].split(split, -1);
result.add(v1);
}

return wildcardMatcher(Utils.listToArray(result));
}
}

主要註解意義:

- @HttpServer: 介面類上註解,非必填,可設定整個介面類的baseUrl
- @Filters: 用於傳遞reat-assured過濾器註解,接收一個io.restassured.filter.Filter類陣列,
如果註釋在類上,則整個類介面都會使用該註解裡面的過濾器,如果註解在方法上,則作用域該方法
- @Get/@Post: 定義介面請求方法,作用於方法上,需要設定介面請求url,介面簡介,如果url為帶http/https則最終請求url為該url
如果不帶http/https,則最終請求urlbaseUrl + url,該註解可設定請求型別:content-type與請求字元編碼集charset
- @HeadMap 作用於請求引數上,設定請求頭,引數型別:Map
- @Head 作用於請求引數上,設定請求頭,需要設定value值,請求時候valuekey,引數為value,可設定多組,如果
同時設定@HeadMap@Head則會合併為一個map
- @paramsForm 作用於請求引數上,設定請求引數,為k-v型別,註解vaule為請求k,引數為v,可多個,合併處理
- @ParamsJson 作用於請求引數上,設定請求引數,json模式,請求方式為json
- @ParamsMap 作用於請求引數上,設定請求引數,引數型別:Map

然後通過testng 的xml配置檔案配置執行

2.迭代1.0.0(直接跳到現在版本,中間迭代太多)

主要問題是寫一個用例耗時太多,需要改太多東西, 然後就有了思考空間, 把不需要的細節隱藏

2.1 引入yaml描述介面與用例

name: login
type: json
description: 登入成功
url: /login
method: POST
headers:
x-request-client-imei: "222222222222"
requests:
{
"phone": "${phone, 13000000001}",
"code": "0000"
}
setup:
- method: createTimestamp
- method: setUptest
args: ${request}
teardown:
- method: teardowm
args: args1111, args222
- method: teardowm1
args: ${response}, ${timestamp}
onFailure:
- method: onFailure
args: args1111, args222
validate:
eq: ["result": 0, "error_code": "0"]
notNull: ["data.token"]
plugin:
- method: "teardowm"
args: args1111, ${orderId}
saveGlobal: ["orderId": "response.data.token"]
saveMethod: ["orderId": "response.data.token"]
saveClass: ["orderId": "response.data.token"]
saveThread: ["orderId": "response.data.token"]
parameters:
- name: login-phone-unregistered
description: 手機號未註冊
headers:
requests:
"phone": "13000000009"
"code": "0000"
validate:
eq: ["result": 1]
- name: login-phone-not-found
description: 手機號不存在
requests:
"phone": "10000000000"
"code": "0000"
validate:
eq: ["result": 1]
- name: login-code-length-error
description: 驗證碼長度錯誤
requests:
"phone": "10000000000"
"code": "000"
validate:
eq: ["result": 1]
- name: login-code-error
description: 驗證碼錯誤
requests:
code: "0001"
validate:
eq: ["result": 1]

2.2 BaseHttpClient , Response類的一些處理

- BaseHttpClient物件方法
wait(),wait(TimeUnit unit, long interval) 用於設定請求介面前的等待時間
saveAsk(),saveGlobal(), saveTest(),saveSuite() 用於往不同生命週期儲存一個快取,saveAsk為該請求生命週期
doHttp(BaseModel model)介面呼叫,入參為model,SINGLE模式時候直接傳入方法入參BaseModel即可
doHttp(String modelName)介面呼叫,入參為modelName, MULTIPLE模式時候傳入modelName即可
- Response物件方法
then(): 語法糖,無特殊意義,只用作鏈式呼叫標明
statusCode(): 用於斷言介面返回code
validate(): 斷言方法
eq(): 硬編碼斷言相等
eqByPath(): 硬編碼斷言相等,值取jsonpath,xpath
validatePlugin(): 硬編碼斷言,用於呼叫方法
saveGlobal(), saveTest(),saveSuite() : 結果儲存不同維度方法
onFailure(BaseFailHandle failHandle): validate()斷言失敗後會執行的方法,所以必須在validate()方法後呼叫,入參為BaseFailHandle介面,需要實現該介面並且重寫handle(T t)方法
onFailure(Class clazz): 同上
extract(): 用於取值
processor(BaseProcessorHandle processorHandle)
processor(Class clazz):用於該呼叫該介面後一些自定義處理,如訂單行程需要一分鐘,入參為BaseProcessorHandle介面,需要實現該介面並且重寫processor(T t)方法
wait(): 介面執行完後等待時間
done(): 用於處理結束,丟擲validate()異常,如沒吊用extract()方法取值的話該方法為鏈式呼叫結尾必須呼叫
auto(): 自動解析yaml檔案所有內容
auto(int httpStatusCode): 自動解析yaml檔案所有內容,手動設定httpStatusCode
autoExcludeDone(): 自動解析yml檔案, 但是不會自動呼叫done()方法結束,需要手動呼叫done結束,主要用於給該http請求新增更多的自定義處理

2.3 然後testng類configuration配置方法不支援引數化,對此進行的一些加強處理

@ApiBeforeMethod
@ApiBeforeClass
@ApiBeforeSuite
@ApiAfterMethod
@ApiAfterClass
@ApiAfterSuite
主要結合@DataModel, @DataFile, @DataParams 當配置方法入參使用

2.4 資料驅動核心註解@DataModel

/**
*
* <p>資料驅動 yaml模式
* <p> 當Format= SINGLE, vaule = {"login", "login1"}
* 表示該用例是單介面模式
* 用例入參模型為: BaseModel
* 入參值為yaml文件name對應的login與login1和這兩個name下的所有parameters
* eg.
* @DataModel(value = {"login", "login1"}, format = DataModel.Format.SINGLE)
* public void login(BaseModel model) {
* apiClient.doHttp(model).auto();
* }
*
* <p> 當Format= MULTIPLE, vaule = {"login", "profile"}
* 表示該用例是業務流模式,只會執行一次,業務對應入參可從
* 用例入參模型為: MultipleModel
* value可省略
* eg.
* @DataModel(format = DataModel.Format.MULTIPLE)
* public void login1(MultipleModel model) {
* driverApiClient.doHttp(model.getModel("login")).auto();
* driverApiClient.doHttp(model.getModel("profile")).auto();
* }
*
*/

@Retention(RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.METHOD})
public @interface DataModel {
/** 取yaml的name */
String[] value() default {};

/** yaml檔名字,支援多個檔案引入,預設路徑為 resources下的data.yml */
String[] path() default {"data.yml"};

/** 模式 */
DataModel.Format format() default Format.MULTIPLE;

enum Format
{
/**單介面**/
SINGLE,
/**序列**/
MULTIPLE
}

2.5 spring功能支援多環境整合與中間的一些配置解釋

application-qa.yml,application-uat.yml 區分環境配置檔案,最終會根據使用環境預設合併到application.yml

- application.yml

- notification節點: 配置通知型別
- retry節點: 配置用例失敗重試

- application-qa.yml,application-uat.yml

- httpurl節點: 配置介面層介面類上面@HttpServer註解中baseurl代表值baseurl直接用${driverapi.url}呼叫即可

2.6 測試結果通知支援釘釘,郵件等,通過application.yml notification節點配置

2.7 引數化說明

yaml模板中支援引數化|jsonpath|xpath等寫法,如${phone, 13000000001}, 該用法為該引數化值設定一個default,如果快取中無該值,那就取default
jsonpath一般用於validatesaveGlobal,用於取返回值校驗與儲存
setup支援${response}引數化,${response}會轉換成BaseModel
teardown支援${response}引數化,${response}會轉換成Response

2.8 因為是springboot架構, 可直接整合JdbcTemplate,redisTemplate,mongoTemplate等

2.9 兩個生命週期監聽器用於擴充

public interface HttpPostProcessor extends PostProcessor{

/**
* http請求之前處理器
* @param context
*/

void requestsBeforePostProcessor(HttpContext context);

/**
* http請求之後處理器
* @param context
*/

void responseAfterPostProcessor(HttpContext context);


/**
* http請求後 對response物件進行各種處理後的處理器
* 在{@link Response done()}內呼叫
* @param context
*/

void responseDonePostProcessor(HttpContext context);
public interface TestNgLifeCyclePostProcessor extends PostProcessor{

/**
* 測試方法執行前執行
* @param result
*/

void onTestMethodStartBeforePostProcessor(ITestResult result);

/**
* 測試方法執行成功後執行
* @param result
*/

void onTestMethodSuccessAfterPostProcessor(ITestResult result);


/**
* 測試方法執行失敗後執行
* @param result
*/

void onTestMethodFailureAfterPostProcessor(ITestResult result);

/**
* 跳過測試方法後執行
* @param result
*/

void onTestMethodSkippedAfterPostProcessor(ITestResult result);

/**
* 在例項化測試類之後且在呼叫任何配置方法之前呼叫
* @param context
*/

void onTestClassInstantiationAfterPostProcessor(ITestContext context);

/**
* 在執行所有測試並呼叫其所有配置方法之後呼叫
* @param context
*/

void onAllTestMethodFinishAfterPostProcessor(ITestContext context);

/**
* suite執行之前執行 對應test.xml suite標籤
* @param suite
*/

void onSuiteStartBeforePostProcessor(ISuite suite);

/**
* suite執行後執行 對應test.xml suite標籤
* @param suite
*/

void onSuiteFinishAfterPostProcessor(ISuite suite);

/**
* 配置方法執行前執行(配置方法: beforeTest,AfterTest等)
* @param result
*/

void onConfigurationStartBeforePostProcessor(ITestResult result);

/**
* 配置方法執行成功時執行
* @param result
*/

void onConfigurationSuccessAfterPostProcessor(ITestResult result);

/**
* 配置方法執行失敗時執行
* @param result
*/

void onConfigurationFailureAfterPostProcessor(ITestResult result);

/**
* 配置方法跳過時執行
* @param result
*/

void onConfigurationSkipAfterPostProcessor(ITestResult result);

2.10 完美相容allure註解

2.11 完美相容testng用法, 只做testng增強

2.12 完美相容maven, 指定testng用例執行, 支援多種testng執行緯度: XML Files, Groups, Parallel,verbosity,'testnames' in test tag

2.13 提供com.ly.core.actuator.TestNgRun 編碼方式執行用例

2.14 提供一個har格式轉換為yaml用例資料(charles匯出.har檔案轉換為yaml格式)

2.15 jenkins支援

3. example

3.1 先編寫yaml (yaml 預設放在resource目錄下,也可直接指定路徑)

testCase:
- name: login
description: 登入
type: json
url: /v1/security/login
method: POST
setup:
- method: createTimestamp
- method: setUptest
args: ${request}
headers:
timestamp: '1589441750400'
os: android9
content-type: application/json;charset=UTF-8
ver: 2.2.0
requests:
code: '0000'
phone: '13000000001'
validate:
notNull: [result]
eq: [result: 0]
len: [result: 1]
hasKey: [data: token]
hasValue: [data: 4]
saveMethod: ["token": "data.token"]
teardown:
- method: teardowm
args: args1111, args222
- method: teardowm1
args: ${response}, ${timestamp}
parameters:
- name: login-phone-unregistered
description: 手機號未註冊
requests:
"phone": "13000000009"
"code": "0000"
validate:
eq: ["result": 1]
- name: login-phone-not-found
description: 手機號不存在
requests:
"phone": "10000000000"
"code": "0000"
validate:
eq: ["result": 1]
- name: login-code-length-error
description: 驗證碼長度錯誤
requests:
"phone": "10000000000"
"code": "000"
validate:
eq: ["result": 1]
- name: login-code-error
description: 驗證碼錯誤
requests:
code: "0001"
validate:
eq: ["result": 1]
- name: login-phone-isNull
description: phone欄位不存在
requests:
"phone": null
"code": "0000"
validate:
eq: ["result": 1]

- name: index
description: 首頁
type: form
url: /v1/driver/index
method: GET
headers:
authorization: ${token}
timestamp: '1589441751257'
os: android9
ver: 2.2.0
requests: {}
validate:
notNull:
- result
eq:
- result: 0

3.2 編寫apiClient, http.test.url寫在配置檔案中

@HttpServer(baseUrl = "${http.test.url}")
@Filters({RestAssuredLogFilter.class})
public interface DefaultApiClient extends BaseHttpClient{
}

3.3 編寫用例

@Story("登入模組介面")
public class ExampleApiTestCase extends BaseDefaultApiTestCase {

@DataModel(value = {"login"},
format = DataModel.Format.SINGLE,
path = {"example.yml"})
@ApiBeforeClass
public void beforeClass(BaseModel model) {
System.out.println("===========beforeClass============: " + model);
}

@DataModel(value = {"login"},
format = DataModel.Format.SINGLE,
path = {"example.yml"})
@ApiAfterMethod
public void afterMethod(BaseModel model) {
System.out.println("=============afterMethod==========" + model);
}



@Severity(SeverityLevel.CRITICAL)
@Description("登入")
@Test(groups = "example")
@DataModel(value = {"login"},
format = DataModel.Format.SINGLE,
path = {"example.yml"})
public void login(BaseModel model) {
apiClient.doHttp(model).auto();
}

@Severity(SeverityLevel.CRITICAL)
@Description("獲取司機資訊")
@Test(groups = "example")
@DataModel(format = DataModel.Format.MULTIPLE, path = "example.yml")
public void order(MultipleModel model) {
apiClient.doHttp("login") //呼叫yaml中name為 login的介面
.processorByExpr("token", RedisDelProcessorCallback.class) //呼叫完做一些處理
.eqByPath("${result}", 0) //斷言
.saveMethod("token", "token") //儲存作用域為method的快取
.onFailure(CancelFailHandle.class) //如果失敗執行失敗兜底處理
.done();// 結束

apiClient.doHttp("index").auto();
}

3.4 testng.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<!--tests級別:不同test tag下的用例可以在不同的執行緒執行,相同test tag下的用例只能在同一個執行緒中執行。-->
<!--classs級別:不同class tag下的用例可以在不同的執行緒執行,相同class tag下的用例只能在同一個執行緒中執行。-->
<!--methods級別:所有用例都可以在不同的執行緒去執行。-->
<!--thread-count: 併發執行緒數-->
<suite name="自動化">
<test verbose="5" name="example" >
<groups>
<!--groups分組-->
<define name="test">
<include name="example" />
</define>

<!--執行的groups-->
<run>
<include name="test" />
</run>
</groups>
<classes>
<class name="com.example.ExampleApiTestCase" />
</classes>
</test>
</suite>

3.5 測試報告 (隨便搞了個)

4.寫在最後

如果小夥伴有興趣的話我把業務程式碼清理下,開源, 原始碼大概1w多行吧,裡面各種其他處理,也希望能收到各位寶貴的意見和建議

相關文章