接口自動化落地分享與總結


一、前言

繼去年下半年給BPM項目做了一個UI自動化,從落地到比較滿意的收益來看,合適的自動化對於測試效率的提升還是比較明顯的。

這次因為接了另外一個項目的部分需求,也是比較復雜且定制化程度較高的需求,然后在測試的過程中跟開發溝通時了解到了Controller層的接口都是開發自己加Swagger注解自測的,同時也了解到開發只會測一下他認為改動代碼時會影響到的接口,而且開發自測基本也是只測下接口能調通就不管了,接口的健壯性和其他接口都可能存在潛在風險,然后數據交互基本都是json格式的,加上有Swagger注解,比較好開展接口自動化,於是和項目組負責人和開發溝通之后便由我來做接口測試並落地接口自動化,就打算利用工作之余和晚上加加班來搞下接口自動化。

二,技術選型

前段時間剛好在Testerhome上看到一篇關於Rest-Assured框架的接口自動化思路,研究了幾天之后就用企業微信的api跑了下demo,自定義注解+動態代理方式的思路確實是值得借鑒學習,於是打算應用到這次的項目上。

三,實施過程

3.1,准備工作

 做接口自動化之前,首先要明確自己項目中的需求:

 1)測試用例數據的存放與加載

 2)測試用例的數據支持多輪運行,有些測試用例數據不能寫死,我這邊是寫成可變標識符${變量名},然后根據反射來生成需要的測試數據

 3)我這邊有用到restful風格的接口,所以針對這類url可變的接口,定義接口時為{},在執行請求前替換成實際的值即可

 4)針對於我們自己的業務流程測試,每一支流程都有不同的processDefKey,所以需要根據每個流程來寫各自的業務流程腳本和單獨設計各自的測試用例數據

 5)用例執行出錯時,有詳細的日志幫助定位

 6)較好的可維護性和可擴展性

3.2,框架搭建

3.2.1 環境搭建

 環境搭建時,主要使用以下技術:

  • SVN:管理代碼工程
  • TestNG:作為測試框架
  • MySql:存放測試用例數據
  • JDBCUtils:訪問數據庫
  • FastJson:處理json格式數據
  • Rest-Assured:請求發送與響應斷言
  • Maven:管理依賴包
  • Log4j:管理日志
  • ExtentTestNGIReporter:適用於接口自動化的測試報告展示

3.2.2 項目結構:分層設計、解耦


 項目中的各個包的作用已經標示在圖中 ,來看一下動態代理+自定義注解方式的具體實現

1,首先是接口定義,直接從SwaggerUI上復制過來,非常方便,然后加上需要的自定義注解,一般一個接口需要請求url、請求方法,請求參數/請求體、接口描述等,參照SwaggerUI上的即可

自定義注解POST

接口定義

 

2,接口定義好之后,在具體的接口測試類中,通過動態代理生成實體,直接調用接口方法,有參數傳入參數,就可以拿到response進行斷言了 

 

3,ProxyUtils動態代理實現的主要細節:拿到各自的注解后解析,再把解析后的數據封裝到測試執行實體中,調用請求封裝工具類HttpUtils的方法傳遞測試執行實體數據即可,HttpUtils篇幅比較長就不貼出來了,可以去文末的github地址里看源碼

 以上便完成了一個接口從定義到寫測試類到完成接口調用的過程了 

3.2.3 測試用例數據的存放與加載

 存放:我這邊是把測試用例數據存放到MySql數據庫中,根據Controller->DataProvider->表,一個Controller對應一個DataProvider對應一張表,這樣做目錄結構統一,可以很直觀的編寫及維護測試用例數據

 加載:BaseTest中setUp時加載所有表的測試用例數據到集合中,DataProvider里根據每個接口測試類的唯一api_id來加載對應接口測試類的數據給到各個接口測試類

 實現細節如下:

 表設計/controller層目錄結構/dataprovider層目錄結構

 表結構設計

 部分新增接口測試用例數據

 

 BaseTest中

 
         
@BeforeSuite
public void setUp() {
logger.info("加載所有測試用例數據");
loadDBTestCaseDatas();
logger.info("其他動作");
}

private void loadDBTestCaseDatas() {
String tableNames = PropertiesUtils.getProperty("testdataTables.properties", "tableNames");
if (tableNames == null || tableNames.trim().length() == 0) {
throw new RuntimeException("測試用例數據表為空!");
}
for (String tableName : tableNames.split(",")) {
logger.info("加載" + tableName + "表測試用例數據");
JDBCUtils.loadTestData(tableName);
}
logger.info("加載參數化變量數據");
JDBCUtils.loadVariable();
logger.info("加載業務流程測試用例數據");
JDBCUtils.getProcessFormDatas(testCases);
}

 DataProvider中

 
         
// 正常新增數據字典
private static String normal_add_api_id = "DDNormalAdd";
// 異常新增數據字典
private static String abnormal_add_api_id = "DDAbnormalAdd";

@DataProvider(name = "normalAddDataProvider")
public Object[][] normalAddDataProvider() {
Object[][] datas = TestCaseUtils.getCaseDataByApiId(normal_add_api_id, fieldNames);
return datas;
}

@DataProvider(name = "abnormalAddDataProvider")
public Object[][] abnormalAddDataProvider() {
Object[][] datas = TestCaseUtils.getCaseDataByApiId(abnormal_add_api_id, fieldNames);
return datas;
}

 測試類中接收DataProvider提供的數據來執行測試即可

 
         
@AfterClass
public void tearDown() {
// 調用刪除接口刪掉新增的數據
dataDictionaryDelete.dataDictionaryDelete(Arrays.toString(dataIds)).then().statusCode(200);
}

@Test(dataProvider = "normalAddDataProvider", dataProviderClass = DataDictionaryAddDataProvider.class, description = "正常新增數據字典")
public void normalAdd(String case_id, String api_id, String query_param, String query_body, Integer expected_code,
String expected_message, String expected_targeted, Integer positive, String description) {
query_body = VariableUtils.handleVariable(query_body);
response = dataDictionaryAdd.dataDictionaryAdd(query_body);
dataIds[i++] = response.jsonPath().getInt("data.id");
// 斷言
Assert.assertEquals(true, AssertUtils.getAssertResult(response, expected_code, expected_message, expected_targeted));
}

3.2.4 通過反射生成可變參數

 可以看到上面的用例表里有一個可變參數${datadictionaryvalue},可變參數表中存儲反射相關信息, 在執行請求之前,先調用處理可變參數方法處理完可變參數

 VariableUtils中

 
         
/**
* 處理可變參數
* @param variableStr
* @return
*/
public static String handleVariable(String variableStr) {
for (Variable variable : BaseTest.variables) {
String variable_name = variable.getVariable_name();
if (variableStr.contains(variable_name)) {
// 獲取反射類名和反射類方法
String reflect_class = variable.getReflect_class();
String reflect_method = variable.getReflect_method();
try {
Class clazz = Class.forName(reflect_class);
Object obj = clazz.newInstance();
Method method = clazz.getMethod(reflect_method);
String result = (String) method.invoke(obj);
variableStr = variableStr.replace(variable_name, result);
} catch (Exception e) {
e.printStackTrace();
GlobalVar.logger.info("反射類【%s】的反射方法【%s】執行失敗,未生成可變參數%s", reflect_class,reflect_method,variableStr);
}
}
}
return variableStr;
}

3.2.5 處理restful風格的接口的可變URL

 先定義一個URL可變參數的注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface PathVariable {
    String value() default "";
}

 然后在接口定義上添加這個注解

public interface CoreFormTemplate {
    @POST(path = "/core/form/template/{}", description = "獲取表單模板")
    Response coreFormTemplate(@PathVariable String processDefKey,
                              @Body String body);
}

 動態代理工具類里解析方法注解時處理

else if (annos[0] instanceof PathVariable) {
     path = path.replaceFirst("\\{}", args[i].toString());
}

 這樣我們在發起請求時直接傳入實際的參數即可

response = coreFormTemplate.coreFormTemplate(processDefKey, query_body);

3.2.6 業務流程測試

 因為業務流程測試是需要調用幾個接口組裝到一起執行的,在不改表結構的情況下,使用DataProvider不能滿足需求了,所以一個業務流程的測試用例數據,會放到對應的測試用例的集合中

// 正向審批全流程api_id和測試用例數據集合
private final String positiveApproval_api_id = "SATPositiveApproval";
private Map<String, Object> positiveApprovalMap = new HashMap<>();
// 重新分派審批全流程api_id和測試用例數據集合
private final String reassignApproval_api_id = "SATReassignApproval";
private Map<String, Object> reassignApprovalMap = new HashMap<>();
// 審批過程中作廢api_id和測試用例數據集合
private final String invalidApproval_api_id = "SATInvalidApproval";
private Map<String, Object> invalidApprovalMap = new HashMap<>();

表數據設計

在測試開始前加載對應的測試用例數據到對應的集合中

@BeforeClass
public void setUp() {
// 加載各個業務流程用例的測試數據
BaseTest.processDatas.forEach((key, value) -> {
String targetApiId = key.split("_")[0];
switch (targetApiId) {
case positiveApproval_api_id:
positiveApprovalMap.put(key, value);
break;
case reassignApproval_api_id:
reassignApprovalMap.put(key, value);
break;
case invalidApproval_api_id:
invalidApprovalMap.put(key, value);
break;
}
});
}

測試用例腳本

@Test(description = "正向審批全流程")
public void positiveApproval() {
// 調模板接口,獲取formDefId
TestCase coreFormTemplateTestCase = (TestCase) positiveApprovalMap.get(positiveApproval_api_id + "_CoreFormTemplate");
String coreFormTemplateBody = coreFormTemplateTestCase.getQuery_body();
response = coreFormTemplate.coreFormTemplate(processDefKey, coreFormTemplateBody);
String formDefId = response.jsonPath().getString("data.formDefId");
// 調保存接口,獲取processInstanceId
TestCase coreSaveTestCase = (TestCase) positiveApprovalMap.get(positiveApproval_api_id + "_CoreSave");
String coreSaveBody = coreSaveTestCase.getQuery_body();
response = coreSave.coreSave(formDefId, VariableUtils.handleVariable(coreSaveBody));
processInstanceId = response.jsonPath().getString("data.processInstanceId");
// 調提交接口
TestCase coreCompleteTestCase = (TestCase) positiveApprovalMap.get(positiveApproval_api_id + "_CoreComplete");
String coreCompleteBody = coreCompleteTestCase.getQuery_body();
response = coreComplete.coreComplete(processInstanceId, VariableUtils.handleVariable(coreCompleteBody));
// 調同意接口
TestCase coreAgreeTestCase = (TestCase) positiveApprovalMap.get(positiveApproval_api_id + "_CoreAgree");
String coreAgreeBody = coreAgreeTestCase.getQuery_body();
// 三次同意
for (int i = 0; i < 3; i++) {
response = coreAgree.coreAgree(processInstanceId, VariableUtils.handleVariable(coreAgreeBody));
}
// 調提交接口
response = coreComplete.coreComplete(processInstanceId, VariableUtils.handleVariable(coreCompleteBody));
// 斷言docStatusLabel=已完成
response.then().body("data.docStatusLabel", equalTo("已完成"));
}

3.2.7 日志記錄

 良好的日志輸出是幫助定位問題的關鍵環節,接口測試主要需要記錄的日志包括:請求的url、參數、方法、請求頭、實際響應、期望響應等等,Rest-Assured提供了log().all()能log請求/響應的所有數據。貼一下我的log4j配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration>

    <!-- Appenders -->
    <appender name="CONSOLE.ERR" class="org.apache.log4j.ConsoleAppender">
        <param name="target" value="System.err" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d [%p] %c - %m%n" />
        </layout>
        <filter class="org.apache.log4j.varia.LevelRangeFilter">
            <param name="LevelMin" value="warn" />
            <param name="LevelMax" value="fatal" />
            <param name="AcceptOnMatch" value="false" />
        </filter>
    </appender>

    <appender name="CONSOLE.OUT" class="org.apache.log4j.ConsoleAppender">
        <param name="target" value="System.out" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d [%p] %c - %m%n" />
        </layout>
        <filter class="org.apache.log4j.varia.LevelRangeFilter">
            <param name="LevelMin" value="debug" />
            <param name="LevelMax" value="info" />
            <param name="AcceptOnMatch" value="false" />
        </filter>
    </appender>

    <!--按天生成日志文件-->
    <appender name="dailyRollingFile"
              class="org.apache.log4j.DailyRollingFileAppender">
        <param name="Threshold" value="debug"/>
        <param name="File" value="test-output/logs/log.log"/>
        <param name="DatePattern" value="'.'yyyy-MM-dd'.log'"/>
        <param name="Encoding" value="UTF-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d [%p] %c - %m%n"/>
        </layout>
    </appender>

    <logger name="com.errout">
        <level value="debug" />
    </logger>

    <!-- Root Logger -->
    <root>
        <priority value="info" />
        <appender-ref ref="CONSOLE.ERR" />
        <appender-ref ref="CONSOLE.OUT" />
        <appender-ref ref="dailyRollingFile" />
    </root>

</log4j:configuration>  

4,成效

 首先就是Bug修復后的回歸驗證了,還是能發現一些修bug引起的其他問題

 然后作為開發冒煙的一部分,目前這套接口自動化已經搭建到Jenkins上,開發在提測前會先去跑一遍,沒有問題才會提測,對於測試和開發算是雙贏

 再就是線上監控,測試用例集里的positive_case這部分用例也放在Jenkins上單獨配置了一個定時任務,H/8每八小時執行一次,有失敗用例會發V消息報警

 最后項目上線的驗證也全部交給自動化來驗證了 

5,源碼地址(實戰企業微信服務端API)

 https://github.com/lynnk1ng37/qywx-service-api.git

 


免責聲明!

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



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