动态

详情 返回 返回

一種基於閉包函數實現自動化框架斷言組件的設計實踐 | 京東物流技術團隊 - 动态 详情

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 轉載請註明來源

user avatar bytebase 头像 wric 头像 weidejianpan 头像 nihaojob 头像 lxlu 头像 mimangdeyangcong 头像 eolink 头像 youyudetusi 头像 weixiaodehai_cywv9b 头像 xiaohuaihuai_5e74ba460d087 头像
点赞 10 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.