專欄文章 質量保障系統的落地實踐 (三) CI 管理設計 - 基礎設計

亦攸發表於2024-05-11

往期文章

專欄文章 質量保障系統的落地實踐 (一) 概覽篇
專欄文章 質量保障系統的落地實踐 (二) 專案管理設計 - 基礎資訊與缺陷資訊設計
效能度量 專欄文章 質量保障系統的落地實踐 (二) 專案管理設計 - 程式碼資訊設計

前言

往期文章中已經介紹了關於專案管理的主要設計內容,圍繞專案管理還有一些小工具的開發,暫且按下不表。後續有機會的話,以工具角度單獨聊聊。
本篇文章主要介紹 CI 管理,也是整個質量保障體系中比較重要的一環,值得 QA 人員探索。

CI 與自動化

首先我們要知道,CI 與自動化的關係。一般來說,測試行業內講自動化,大部分是指介面自動化,當然也不排除 UI 自動化,狹義上指的是測試人員需要有編寫、除錯、執行自動化指令碼的能力。廣義上指測試具備使用程式碼解決一些問題的能力,用機器替代人力。
CI 的含義是持續整合,顧名思義,兩個要素:1、整合;2、持續。需要將一些能力 (包括但不限於程式碼) 組合在一起,完成某項或多項訴求,並且可以繼續往後迭代,不斷豐富能力。
所以綜上來說,CI 的語境範圍是大於自動化的。那麼投射到測試行業內,CI 可具象化成:

隨著專案不斷地迭代,業務場景會逐漸增多,當然業務場景也有優先順序的差別。資源有限的情況下優先保證主流程的暢通與穩定,專案需求的更新必將不斷增加業務場景的複雜程度,也將增加業務程式碼處理的複雜程度,特別伴隨在專案中還存在著的人員迭代 (離職 or 新加入),後續進入專案的人,對於需求的熟悉程度有限。這種情況下純依賴某位或多位測試人員對於專案以往需求的熟悉程度來把控專案質量是有風險的,需要有自動化的輔助。
直接參與專案的測試人員在專案釋出後,迅速梳理出核心場景,並完成高質量的自動化指令碼,於測試環境進行核心場景除錯。除錯透過後,整合到現有的業務自動化體系中除錯,若導致上下游的指令碼出現了異常,則需要排查原因,解決問題後重新除錯,直至整合測試透過。
測試環境透過後,相同的方式依次在預發環境及線上環境重複,保證新增專案的自動化指令碼與現有業務自動化體系完美契合。
由上面的論述可知,高質量的自動化指令碼是釋放測試人力,替代人力監控系統核心業務的重要幫手,那麼對於自動化指令碼的編寫自然有了要求,期望越細緻越好。那麼針對這個要求,我們該怎麼來設計?

案例選型

自動化可總體分為介面自動化和 UI 自動化,本篇文章以介面自動化為例,一來是我司更偏重於介面自動化;二來是在我司介面自動化的實踐已有一定成果,適合作為案例分析。若是閱讀本篇文章的同學採用的是 UI 自動化或其他自動化,我想也可當做休閒文章閱讀。

介面資訊設計

既然本篇文章的切入點在介面自動化上,那麼首先映入眼簾的就是介面資訊的設計。由於我們的需求是要管理團隊的自動化指令碼質量,團隊由測試成員組成,那麼很自然的,這個資料也是需要落到具體某位測試身上的,聚合而得團隊的資料。
業務核心場景由介面串聯而成,介面是承載業務場景的底座,結合人員資料的需求,很自然的我們就會考慮將介面資訊與測試關聯起來:

# CI管理-介面管理-服務介面資訊表
class CIInterfaceInfo(BaseModel):
    department_id = models.IntegerField(verbose_name=u"歸屬組織節點")
    app_code = models.CharField(max_length=50, verbose_name=u"服務code")
    domain = models.CharField(max_length=150, verbose_name=u"域名")
    path = models.CharField(max_length=255, verbose_name=u"介面路徑")
    description = models.CharField(max_length=40, verbose_name=u"介面描述")
    is_core = models.BooleanField(default=False, verbose_name=u"是否為核心介面")
    priority = models.IntegerField(verbose_name=u"介面優先順序")
    owner_name = models.CharField(max_length=11, verbose_name=u"負責人姓名")
    owner_phone = models.CharField(max_length=11, verbose_name=u"負責人手機號")
    is_available = models.BooleanField(default=True, verbose_name=u"是否有效")
    env = models.IntegerField(verbose_name=u"歸屬環境(測試/預發/生產)")

    class Meta:
        db_table = 'CI_Interface_Info'
        verbose_name = "CI管理-CI介面資訊"
        verbose_name_plural = verbose_name

透過上面的資料表設計,我們得到的介面與測試人員的關係圖如下:

校驗粒度設計

前文中一直在強調高質量自動化指令碼,那麼怎麼樣去界定這個高質量,或者說該設計一種怎樣的機制去促使測試寫出高質量的自動化指令碼?若是應付交差而已,測試人員在編寫自動化指令碼的時候,完全可以不做任何校驗或僅僅保證介面能夠訪問成功,這在實際工作中是完全有可能出現的,那麼該怎麼做?
首先想要改善某個指標,往往是先發現這個指標不令人滿意,隨後需要查出有多少這類情況資料,然後想辦法去提高,總結來說以資料驅動管理,這也是我設計質量保障平臺的一個核心觀點。
以我所在公司的情況為例,由於我負責自動化的整合,所以會時常 review 自動化程式碼。在此過程中發現了很多介面很少甚至沒有任何必要欄位校驗,但是存量資料又太多,不可能一一去翻閱然後統計。那麼該怎麼做?必然是要使用程式碼的,問題又來了,程式碼怎麼知道一個介面的校驗是不是足夠呢?答案是不能的,對於一個介面的校驗是否到位除了參與該介面所屬業務的測試外,其他人都很難評估,這時候怎麼辦?
回頭想想自己是怎麼判斷某個介面的校驗是否完善,直觀感受是校驗語句的多少,若是校驗的細緻,那麼校驗語句一定使用得多,那麼這個指標可以作為一項判斷依據。有了這一項是不是就足夠了呢?實際情況中確實有的介面只是很簡略的處理邏輯,那是不是意味著這樣的介面就是校驗不到位的呢?解決這個問題的方案是看實際的傳參情況,例如一個提交表單介面,提交 10 個欄位,介面校驗部分少於這個數量甚至沒有,那麼程式可以判定校驗資料不夠。

那麼我們就抽離出了兩個關鍵要素:1、傳引數量;2、校驗語句數量,這就是本小節所述的校驗粒度
而這兩個引數恰恰又是關聯在介面上的:

以上圖為例,實際的自動化執行過程中,最小單元是測試套件->校驗套件->api 觸發套件。從校驗套件中可以提取出傳引數量以及校驗語句數量,而這兩個資料又與介面進行關聯,透過介面資訊表的設計,介面與測試人員已經建立了聯絡,那麼就可以將這兩個關鍵資料與對應測試關聯起來。
分析到這,基本可以滿足我們的訴求,下面再來談談校驗粒度的設計。首先傳引數量是無法維護的,因為每個介面的情況都不同,很難維護;其次校驗語句是可以維護的,因為不管使用哪一類的自動化框架,自研的也好,市面上成熟的體系也好,一定有對應的完整的校驗語句維護,那麼這些資訊我們就可以維護下來,而後可統計介面的校驗套件中包含了多少條校驗語句:

# CI管理-校驗語句管理-校驗語句配置資訊表
class CIVerificationSentenceConfigInfo(BaseModel):
    department_id = models.IntegerField(verbose_name=u"歸屬組織節點")
    sentence = models.CharField(max_length=200, verbose_name=u"校驗語句")
    is_available = models.BooleanField(default=True, verbose_name=u"是否有效")

    class Meta:
        db_table = 'CI_Verification_Sentence_Config_Info'
        verbose_name = "CI管理-CI校驗語句配置資訊"
        verbose_name_plural = verbose_name

粒度資料獲取

這一小節我們來談談怎麼獲取校驗粒度的資料,在這之前我們先談談自動化框架的分層設計。前文也提到了,一般來說分為配置檔案、底層能力、測試套件、校驗套件、api 套件。觸發自動化指令碼之後,由測試套件觸發校驗套件,校驗套件呼叫 api 套件完成介面請求,其中會有呼叫底層能力,讀取配置檔案等等步驟。那麼在執行過程中是很容易拿到包括 api、校驗傳參、校驗語句等關鍵資料的,非常滿足我們的需求,但這個方案卻不一定能落地。這是為什麼?
因為在執行過程中獲取我們期望的引數,這就要求我們的期望邏輯需要侵入到自動化框架執行過程中,需要框架研發者為我們定製邏輯,而通常情況下這種要求是很難滿足的。那麼還有沒有別的辦法?
是有的,既然無法侵入執行步驟,那就暴力解析:無論什麼自動化框架,校驗套件和 api 套件都是檔案,那麼我們直接解析檔案提取我們想要引數即可,這種解析方式就要求了在編寫校驗套件和 api 套件時需要按照一定的規則進行,便於解析匹配:

api_analysis_prefix = models.CharField(max_length=50, verbose_name=u"平臺api解析字首")
api_analysis_postfix = models.CharField(max_length=50, verbose_name=u"平臺api解析字尾")
keyword_analysis_prefix = models.CharField(max_length=50, verbose_name=u"平臺keyword解析字首")
keyword_analysis_postfix = models.CharField(max_length=50, verbose_name=u"平臺keyword解析字尾")

依據於配置的前字尾進行匹配,提取出 api 資料、校驗套件內的傳引數量、校驗語句數量並不難。
以下程式碼以 RF 為例,解析前已經完成了團隊內部程式碼編寫規範要求,api 套件均以 Http-開頭,-api 結尾,所以解析過程比較清晰:

# api.robot檔案範例
Http-XX業務:獲取登入使用者資訊-api
    [Arguments]                              ${SH_TOKEN}                      ${data}=&{EMPTY}
    ${headers}                               Create Dictionary                Token=${SH_TOKEN}            
    Create-Session-XXXX
    ${result}                                HTTP Post                        session_name                               /xxx/xxx/xxx                                   headers=${headers}                            data=${data}
    [Return]                                 ${result}

# CI管理-api.robot檔案解析
def api_robot_analysis(file_path, prefix="Http-", postfix="-api"):
    # 維護Http-型別keyword內容對映關係
    api_map = defaultdict(list)
    key = None

    with open(file_path) as f:
        content = f.readlines()
        # 將keyword及keyword內容包裝
        for line in content:
            line = line.strip()
            if line.startswith(prefix) and line.endswith(postfix):
                key = line
                continue
            if key is not None:
                if line and "HTTP" in line.upper():
                    split_res = re.split('\s+', line.upper())
                    try:
                        http_index = split_res.index("HTTP")
                    except ValueError:
                        continue
                    path_index = http_index + 3
                    try:
                        path = split_res[path_index]
                    except IndexError:
                        continue
                    # 避免註釋介面也被計算在內,僅處理第一條
                    if len(api_map[key]) == 0:
                        api_map[key].append(path.lower())

校驗套件也是類似的,提前約定以 Http-開頭,-keyword 結尾:

# keyword.robot檔案
Http-XXX業務:XXX統一攔截器-keyword
    ${userInfo}                    Http-XXX業務:獲取登入使用者基礎資訊-api
    ### 引數提取 ###
    ${shopCode}                    Set Variable                                          ${userInfo}[shopCode]

    ### 資料查詢 ###
    ${querySql}                    Set Variable                                       select xxx from xxxx where xxxx
    ${shop_record}                 remote                                                query database by env                                    ${querySql}                                           &{DB}
    Should Not Be Empty            ${shop_record}[0]

    ### 引數提取 ###
    ${shop_info}                   Set Variable                                          ${shop_record}[0]
    ${whether_test}                Set Variable                                          ${shop_info}[whether_test]
    # 測試標識攔截
    Should Be Equal                ${whether_test}                                       ${1}

# CI管理-keyword檔案內容分離器
def keyword_content_split(file_path, keyword_prefix, keyword_postfix, inner_keyword_prefix):
    # 以keyword名稱為key,聚合keyword主體
    keyword_line_map = defaultdict(list)
    keyword_key = inner_keyword_key = None

    with open(file_path) as f:
        content = f.readlines()
        for line in content:
            # 移除換行符等干擾字串
            line = line.strip()

            # 過濾無效行
            if not line:
                continue

            # 判斷是否符合待採集的keyword名稱,符合則將keyword名稱設定為key
            if line.startswith(keyword_prefix) and line.endswith(keyword_postfix):
                keyword_key = line
                continue

            if keyword_key is not None:
                # keyword校驗語句去重-預防複製貼上
                if line not in keyword_line_map[keyword_key]:
                    keyword_line_map[keyword_key].append(line)

    return keyword_line_map

分別得到 api.robot 與 keyword.robot 內的資料,只需要做一定的資料分離,如按空格分隔字串等手段,就可以解析出程式碼主體裡的各條語句,與我們自己定義的校驗粒度進行匹配,就可以得到每一個校驗套件 (keyword) 的校驗粒度資料。

小結

本篇文章對於 CI 管理的設計開了一個頭,考慮到結構完整性,下一篇文章再來討論如何將這些邏輯進行整合。感謝大家閱讀。

相關文章