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