一種基於閉包函數實現自動化框架斷言元件的設計實踐

2023-10-23 12:01:09

1 背景

目前測試組同學基本具備自動化指令碼編寫能力,為了提高效率,如何靈活運用這些維護的指令碼去替代部分手工的重複工作?為了達到測試過程中更多的去使用自動化方式,如何能夠保證通過指令碼覆蓋更多的校驗點,提高自動化測試的精度和力度?那麼一定是不斷的豐富斷言,符合預期場景。緊接著棘手的問題就是,在前人維護的指令碼不清楚如果在方法內部修改?擔心修改原來邏輯影響正向流程執行?一個斷言方法希望應用到更多的用例中?本文意在介紹通過閉包函數,實現自動化框架中斷言元件的設計實踐。

2設計方法

2.1 設計思路

隨著指令碼維護量不斷增大,維護的人越來越多,即要增加斷言場景又要保證每天持續整合執行原有用例的成功率。我們理想的斷言元件,一定是在不改變原來用例結構和呼叫方式基礎之上,對前人寫的程式碼零侵入,通過裝飾器增加更多場景斷言,並且做到複用斷言元件到更多的測試用例上。

2.2 原理解讀

2.2.1 閉包函數解讀

名詞解釋:

閉包函數是函數的巢狀,函數內還有函數,即外層函數巢狀一個內層函數,在外層函數定義區域性變數,在內層函數通過nonlocal參照,並實現指定功能,比如計數,最後外層函數return內層函數。

主要作用:

可以變相實現私有變數的功能,即用內層函數存取外層函數內的變數,並讓外層函數內的變數常駐記憶體。

實現原理:

閉包函數之所以可以實現讓外層函數內的變數常駐記憶體,關鍵就是其定義了個內層函數,並通過內層函數存取外層函數的變數,並最後由外層函數將內層函數返回出去並賦值給另外一個變數。此時因為內層函數被賦值給一個變數,其記憶體空間不會被釋放,而內層函數又在其函數體內參照了外層函數的變數,導致該變數的記憶體也不會被回收。一般情況下,當一個函數執行完畢後,其記憶體空間即被回收釋放,下次再呼叫該函數的時候,會重新完整執行一次被呼叫函數,但閉包函數主要是利用Python的記憶體回收機制,實現了閉包的效果。

2.2.2 裝飾器解讀

名詞解釋:

裝飾器自身是一個返回可呼叫物件的可呼叫物件,本質是一個閉包函數。

結構特點:

裝飾器也是函數的巢狀結構,可能還會存在三層巢狀,外層函數就是裝飾器函數,接受的引數是一個函數,一般是傳入被裝飾函數;內層函數實現具體的裝飾器功能,比如紀錄檔記錄、登入鑑權、邏輯校驗等,內層函數return一次傳入的函數呼叫,外層函數return內層函數;如果是多層巢狀,最內層是實現具體裝飾器功能的函數,並負責呼叫一次傳入的函數,最外一層函數return第二層函數,依次類推,不過一般最多就是三層函數巢狀。

3 解決方案

3.1 現有用例

def test_enquiry_bill_for_two_driver_quote_price(params):
    """
    終端來源兩個司機同時報價再修改其一報價
    Args:
        params:測試用例資料


    Returns:測試用例實際返回結果


    """
    # 詢價接單
    enquiry_code = jsf_receive_enquiry_bill(**params['expect'][0]).get("data")
    params['actual'].append({"enquiryCode": enquiry_code})
    # 獲取單趟任務
    transit_job_code = get_transit_job_code(enquiry_code=enquiry_code).get('transit_job_code')
    # 司機報名,報價
    params['expect'][1].update({"transitJobCode": transit_job_code})
    jsf_apply_transit_job_by_param(**params['expect'][1])
    # 第二位司機報名,報價
    params['expect'][2].update({"transitJobCode": transit_job_code})
    jsf_apply_transit_job_by_param(**params['expect'][2])
    # 第二位司機修改報價
    params['expect'][2].update({"quotePrice": 100})
    actual = jsf_apply_transit_job_by_param(**params['expect'][2])
    params['actual'].append(actual)
    assert actual.get('code') == 1
    assert actual.get('message') == '重新報價成功'
    log.info(f'驗證預期結果為 {actual.get("data")} 通過')
    return params

3.2 斷言元件設計

單一業務節點校驗元件:

如上對詢價單報價場景,現有測試用例完全可以單獨執行,目前只有簡單的返回值斷言,缺少很多關鍵節點校驗。比如,步驟一詢價接單是否落庫成功,步驟二單趟任務是否建立成功;步驟三司機報價後的單趟價格,步驟四司機再次提交報價,呼叫介面後的價格是否修改成功,我們為了不影響原來用例執行,對原始碼做到零侵入,且自動實現斷言異常捕獲,可以通過增加一個斷言元件完成。

def validation(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            # 執行函數
            data=func(*args, **kwargs)
            actual_enquiry=hash_db.query_enquiry_bill(data['actual']['enquiryCode'])
            actual_transit=hash_db.query_transit_job_bill(data['expect'][1]['transitJobCode'])
            assert data.get("expect")[2]['quotePrice'] == actual_transit['quote_price']
        except Exception as ex:
            log.exception(ex)

    return wrapper

公共校驗元件:

如上實現了通過一個裝飾器去完成斷言,但有些同學認為,以上斷言方法又不能適用於其他用例,為什麼還要額外重寫一個函數呢?其實這種方式,更多的會應用到公共元件,比如以下通過裝飾器完成用例返回值與對應資料庫的斷言場景。

def validation_db(sql,**kwargs):
    def validation(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                counts, results = tms_mysql.execute_query(sql)
                if counts:
                    # 根據獲取資料開始斷言
                    for key_res, value_res in results[0].items():
                        for key_arg, value_arg in kwargs.items():
                            if field_change(key_res, change_type='to_arg') == key_arg:
                                    log.info(f'斷言{key_arg}欄位,預期值是{value_res},實際值是{value_arg}')
                                    assert value_res == value_arg
                else:
                    return counts
            except Exception as ex:
                log.exception(ex)

        return wrapper
    return validation

3.3 改造用例

單一裝飾器元件

如下所示,用例test_enquiry_bill_for_two_driver_quote_price內部程式碼依舊不變,僅是在方法上,加上

@validation,目前在執行原有用例時,增加校驗過程資料,比如第一次提交報價的值,更改後提交資料的變化,增加現有自動化測試用例的可靠性。

@validation
def test_enquiry_bill_for_two_driver_quote_price(params):
    """
    終端來源兩個司機同時報價再修改其一報價
    Args:
        params:測試用例資料


    Returns:測試用例實際返回結果


    """
    # 詢價接單
    enquiry_code = jsf_receive_enquiry_bill(**params['expect'][0]).get("data")
    params['actual'].append({"enquiryCode": enquiry_code})
    # 獲取單趟任務
    transit_job_code = get_transit_job_code(enquiry_code=enquiry_code).get('transit_job_code')
    # 司機報名,報價
    params['expect'][1].update({"transitJobCode": transit_job_code})
    jsf_apply_transit_job_by_param(**params['expect'][1])
    # 第二位司機報名,報價
    params['expect'][2].update({"transitJobCode": transit_job_code})
    jsf_apply_transit_job_by_param(**params['expect'][2])
    # 第二位司機修改報價
    params['expect'][2].update({"quotePrice": 100})
    actual = jsf_apply_transit_job_by_param(**params['expect'][2])
    params['actual'].append(actual)
    assert actual.get('code') == 1
    assert actual.get('message') == '重新報價成功'
    log.info(f'驗證預期結果為 {actual.get("data")} 通過')
    return params

多個裝飾器巢狀

如下是多個元件巢狀使用方式,及執行順序解讀

@dec1
@dec2
@dec3
def func():
pass

此時:可以對某個被裝飾函數,增加多個功能

裝飾器生效順序,從上到下,即dec1>dec2>dec3

在第一步改造後,僅是增加了對核心欄位的過程資料校驗,有的同學希望用例更加準確,不用再切換去看資料庫,直接將所有返回值欄位,與庫裡進行預期比較。

如下所示,同樣在原有用例上增加多個裝飾器,即多個斷言元件,按順序依次斷言。下面是,增加定義的單個用例的私有斷言@validation和資料庫公共斷言@validation_db
增加後不會影響原來測試流程執行,大家也可以按照需求,在斷言元件內宣告,斷言異常是否中斷。

@validation
@validation_db(enquiry_sql)
def test_enquiry_bill_for_two_driver_quote_price(params):
    """
    終端來源兩個司機同時報價再修改其一報價
    Args:
        params:測試用例資料


    Returns:測試用例實際返回結果


    """
    # 詢價接單
    enquiry_code = jsf_receive_enquiry_bill(**params['expect'][0]).get("data")
    params['actual'].append({"enquiryCode": enquiry_code})
    # 獲取單趟任務
    transit_job_code = get_transit_job_code(enquiry_code=enquiry_code).get('transit_job_code')
    # 司機報名,報價
    params['expect'][1].update({"transitJobCode": transit_job_code})
    jsf_apply_transit_job_by_param(**params['expect'][1])
    # 第二位司機報名,報價
    params['expect'][2].update({"transitJobCode": transit_job_code})
    jsf_apply_transit_job_by_param(**params['expect'][2])
    # 第二位司機修改報價
    params['expect'][2].update({"quotePrice": 100})
    actual = jsf_apply_transit_job_by_param(**params['expect'][2])
    params['actual'].append(actual)
    assert actual.get('code') == 1
    assert actual.get('message') == '重新報價成功'
    log.info(f'驗證預期結果為 {actual.get("data")} 通過')
    return params

4 總結

以上實踐案例,是基於運力測試團隊現有的自動化維護情況,前期指令碼已大量堆砌但缺少斷言,現階段測試流程沒有變化,但為了增加自動化指令碼的測試力度需要批次增加斷言。是否利用裝飾器來實現斷言,一定要取決於團隊中維護用例的情況,如果當前用例從頭到尾都是你一個人維護,裡面的場景也沒辦法給其他人公用,那麼大可不必!不過學習好裝飾器後,在程式碼編寫過程中希望一處實現多處複用,也可以通過裝飾器方式去提升程式碼可讀性和可維護性。

作者:京東物流 劉紅妍

來源:京東雲開發者社群 自猿其說Tech 轉載請註明來源