Categories
程式開發

接口自動化對比工具實踐


背景

接口自動化一直以來都是質量保障的重要一環,在接口自動化日常工作中,我們致力於場景的覆蓋與結果校驗。隨著業務的高速發展,高效保質的迭代自動化用例成了我們的一個研究方向,其中用例結果校驗的及時性、完整性、可維護性是我們遇到的一個很大的難題。

痛點

筆者所屬團隊,日常工作是圍繞商品相關業務展開。在平時的自動化腳本編寫中,我們發現:

  1. 商品模型返回字段多(一個模型一般有幾十到上百個字段),逐字段人工斷言,成本較高;
  2. 商品原自動化工程裡有大量重複的校驗邏輯,梳理成本較高;
  3. 隨著業務發展部分非核心字段逐漸也變成了核心字段,例如商品編碼,現在已經成為了很多商家ERP系統識別商品數據的關鍵標識;
  4. 部分字段更新如何保證其他字段沒有被更新掉,尤其是一些存在默認值的字段,更新的時候極易被默認值覆蓋。

傳統校驗方式我們一般只會校驗核心字段或者用例相關字段,比如:價格、庫存等等。但是基於上述第3、4點原因,我們發現需要去做全字段校驗,而 全字段校驗學習成本高、維護代價大、代碼熟悉程度要求高 是面臨的三大難題,那麼如何做到快速、優雅全字段校驗成為我們必須去解決的問題。

目標

我們的目標是爭取對用例返回字段進行全量校驗,同時也要大幅提升用例編寫效率。

場景分析

我們對現有的自動化用例場景進行分析,得到以下結論:

  • 待測試的後端接口一般分為操作接口和查詢接口兩類;
  • 一個操作類接口落庫後的數據一般會對應一個或者多個查詢類接口;
  • 查詢類接口會返回大量業務字段。

接下來我們分別針對操作、查詢這兩類接口進行處理。

實踐前的準備

為了讓大家更好的理解後續內容,我們先對有贊目前的測試環境進行一個概述:(詳細內容可參見:有贊環境解決方案),環境示意圖如下:

接口自動化對比工具實踐 1

目前有贊測試環境採取的是弱隔離策略,分為基礎環境和測試環境。基礎環境部署應用的代碼分支版本同線上一致,項目環境部署的則是應用特性分支代碼,兩個環境共用一套存儲。當一個業務請求進來時,根據一個標誌位(內部簡稱sc)來判定是否要走到項目環境,如果請求的是項目環境且項目環境有該應用,那麼此請求會被路由到項目環境中,否則請求到基礎環境裡。

實踐

下面介紹一下我們整體思路:

  • 讀接口校驗:分別請求基礎環境和項目環境,對比兩個環境的返回結果,如果一致說明代碼改動對此接口用例沒有影響,進而可以判定用例校驗通過;
  • 寫接口校驗:一般寫接口落庫的數據可以通過一個或者多個讀接口拿到,那麼同樣的寫接口分別在基礎環境和項目環境進行落庫,只要對應環境的讀接口返回結果一致,那麼校驗通過;
  • 不論讀寫,都有一些隨機字段,為了降低接入成本,需要提供計算忽略字段能力。

讀接口校驗思路

讀接口校驗相對簡單,分別請求基礎環境和項目環境,根據返回值的異同來判定用例是否通過。我們可以藉鑑AOP的思路,切入點為dubbo請求前後,在切面中分別請求基礎環境和sc環境,根據兩次返回值來判定用例是否通過。

整體流程如下:

接口自動化對比工具實踐 2

PS:sc環境即為部署了應用特性分支代碼的環境

根據上述流程圖,可以看到重點在忽略字段生成以及比對邏輯,思路如下:

  • dubbo接口返回值基本都是一個對象,參考jsonpath思路,通過遞歸可以獲得一個Map(k:路徑,v:路徑值)。舉個例子,返回值的類為
public class ItemSavedModel {
    private Long itemId;
    private Long shopId;
}

假設返回對象itemId為1,shopId為2,那麼拆解出Map為{“/itemId”:“1”,”/shopId”:“2”},對象的比較轉換為Map的比較。通過兩次基礎環境返回值的比較,不同的路徑值對應的路徑,就是下次比較要忽略的路徑。

對象拆解成Map核心代碼如下:

 /**
     * 获取对象路径
     *
     * @param obj
     * @return
     */
    public static HashMap getObjectPathMap(Object obj) {

        HashMap map = new HashMap();
        getPathMap(obj, "", map);
        return map;
    }
    
    private static void getPathMap(Object obj, String path, HashMap pathMap) {
           if (obj == null) {
               return;
           }
           Class clazz = obj.getClass();
           //基本类型
           if (clazz.isPrimitive()) {
               pathMap.put(path, obj);
               return;
           }
           //包装类型
           if (ReflectUtil.isBasicType(clazz)) {
               pathMap.put(path, obj);
               return;
           }
           //集合或者map
           if (ReflectUtil.isCollectionOrMap(clazz)) {
               //todo:默认key为基础类型
               if (Map.class.isAssignableFrom(clazz)) {
                   Map map = (Map) obj;
                   map.forEach((k, v) -> {
                       if (k != null) {
                           getPathMap(v, path + "/" + k.toString(), pathMap);
                       }
                   });
   
               } else {
                   Object[] array = ReflectUtil.convertToArray(obj);
                   for (int i = 0; i < array.length; i++) {
                       getPathMap(array[i], path + "/" + i, pathMap);
                   }
               }
               return;
           }
   
           //pojo
           //获取对象所有的非静态变量字段
           List fields = ReflectUtil.getAllFields(clazz);
           fields.forEach(field -> getPathMap(ReflectUtil.getField(obj, field), path + "/" + field.getName(), pathMap));
           return;
       }
  • 按照上述代碼拆解出項目環境返回值對應的Map(k:路徑,v:路徑值),根據上一步驟獲得的忽略路徑和基礎環境返回值,即可計算出兩次返回值一致與不一致的字段路徑。代碼不再贅述,感興趣的讀者可以在留言區討論。

寫接口校驗思路

寫接口相對讀接口會復雜一些,篇幅所限,主要講解核心邏輯。寫接口校驗整體邏輯與讀接口類似:總共觸發三次請求,前兩次所有讀寫接口在基礎環境執行,計算出忽略字段以及記錄下來基礎環境返回值。第三次所有請求都在項目環境,獲取接口在項目環境的返回值,接下來排除掉忽略字段,比較基礎環境和項目環境接口對應的返回值即可完成校驗。

整體流程如下:

接口自動化對比工具實踐 3

  • 重試時機很重要:寫接口不同於讀接口,讀接口可以在不同環境裡多次重試,而寫接口考慮到冪等性,在數據清理之前是不能發起重試的。清理數據可能在afterMethod、afterClass、afterTest、AfterSuite各種階段,為了保證數據清理代碼在第二次執行之前被執行,我們考慮實現監聽器方法ISuiteListener.onFinish方法來觸發第二次。
  • 如何觸發第二次執行:TestNG除了通過xml文件觸發,還支持通過新建對象來觸發執行,我們採用新建TestNG對象來觸發第二次執行。
  • 忽略字段計算時機:切面需要在用例執行第2遍時候進行計算忽略字段,執行第3遍進行接口返回值的比較。
//触发三次请求
public class GlobalCoverISuiteListener implements ISuiteListener {

    public static ConcurrentHashMap suiteFinishMap=new ConcurrentHashMap();


    @Override
    public void onStart(ISuite suite) {
        if(suiteFinishMap.size()==0 ){
            //第一次进来 设置globalCoverFlag为1 后面会new testng两次
            System.setProperty("globalCoverFlag", "1");
            if(System.getProperty("globalCoverFlag").equals("1")) {
                suiteFinishMap.put(suite.getXmlSuite().getName(),1);

                TestNG tng = new TestNG();
                tng.setXmlSuites(Arrays.asList(suite.getXmlSuite()));
                tng.run();
            }

        }
    }

    @Override
    public void onFinish(ISuite suite) {

        suite.getResults().forEach((suitename, suiteResult)->{
            ITestContext context = suiteResult.getTestContext();
                if(System.getProperty("globalCoverFlag").equals("1")) {
                    int before = suiteFinishMap.get(suite.getXmlSuite().getName());

                    //第二次结束 表示计算忽略字段已经结束 可以进行正常的跑case了
                    if(suiteFinishMap.get(suite.getXmlSuite().getName())==2){
                        suiteFinishMap.put(suite.getXmlSuite().getName(),++before);
                        System.setProperty("globalCoverFlag", "0");
                        return;
                    }
                    suiteFinishMap.put(suite.getXmlSuite().getName(),++before);

                    TestNG tng = new TestNG();
                    tng.setXmlSuites(Arrays.asList(context.getCurrentXmlTest().getSuite()));
                    tng.run();
                }


        });
    }
}
  • 測試用例中一般都存在著讀、寫接口兩類用例,只有寫操作用例需要在三遍suite中均執行,讀操作用例只需要在最後一次suite執行即可。綜合考慮,前兩次的suite希望只執行寫操作的case。因此實現testNg監聽器方法IMethodInterceptor.intercept,攔截器上只返回此次suite執行的測試用例,從而達到前兩次只執行寫操作的case。我們採用在寫操作用例掛上一個註解,標識為寫操作用例,方便攔截器判斷用例類型。核心代碼如下:
//case层方法加上注解
    @WriteCaseDiffAnnotation
    @Test
    public void testAdd(){
        //写操作
        //读操作
    }

// IMethodInterceptor.intercept中判断注解,获取写操作用例
@Override
    public List intercept(List methods, ITestContext context) {
        List testMethods = new ArrayList();
//获取写操作测试用例方法
        for (IMethodInstance methodInstance : methods) {
                if (isQualified(methodInstance.getMethod())) {
                    testMethods.add(methodInstance);
                }
            }
       

        if (System.getProperty(GlobalOperatorType.GLOBAL_COVER.getStr()).
                equals(GlobalOperatorType.GLOBAL_COVER.getFlag())) {
//前两次suite,只返回写操作测试方法
            return testMethods;
        } else {
//最后一次suite,返回所有的测试方法
            return methods;
        }
    }
//判断测试方法是否有WriteCaseDiffAnnotation注解
    public boolean isQualified(ITestNGMethod iTestNGMethod) {
        Method m = iTestNGMethod.getConstructorOrMethod().getMethod();
        WriteCaseDiffAnnotation writeAnnotation = m.getAnnotation(WriteCaseDiffAnnotation.class);
        Test test = m.getAnnotation(Test.class);
        if (writeAnnotation != null && test != null) {
            return true;
        }
        return false;
    }

不足

  • 目前僅支持dubbo接口,後期考慮擴展到前端node層接口校驗
  • 目前強依賴基礎測試環境,為了更好的兼容性,後期考慮引入存儲方式來解除基礎環境依賴

本文轉載自公眾號有贊coder(ID:youzan_coder)。

原文鏈接

https://mp.weixin.qq.com/s/_9HIrichpu4sXXXASTWVBQ