ApiTestEngine演進之路 (4) 測試用例中實現 Python 函數的調用
在《測試用例中實現Python函數的定義》中,介紹了在YAML/JSON測試用例中實現Python函數定義的兩種方法,以及它們各自適用的場景。
但是在YAML/JSON文本中要怎樣實現函數的調用和傳參呢?
variable_binds:n - TOKEN: debugtalkn - json: {}n - random: ${gen_random_string(5)}n - authorization: ${gen_md5($TOKEN, $json, $random)}n
例如上面的例子(YAML格式),gen_random_string和gen_md5都是已經定義好的函數,但${gen_random_string(5)}和${gen_md5($TOKEN, $json, $random)}終究只是文本字元串,程序是如何將其解析為實際的函數和參數,並實現調用的呢?
本文將對此進行重點講解。
函數的調用形式
在Python語言中,函數的調用形式包含如下四種形式:
- 無參數:func()
- 順序參數:func(a, b)
- 字典參數:func(a=1, b=2)
- 混合類型參數:func(1, 2, a=3, b=4)
之前在《探索優雅的測試用例描述方式》中介紹過,我們選擇使用${}作為函數轉義符,在YAML/JSON用例描述中調用已經定義好的函數。
於是,以上四種類型的函數定義在YAML/JSON中就會寫成如下樣子。
- 無參數:${func()}
- 順序參數:${func(a, b)}
- 字典參數:${func(a=1, b=2)}
- 混合類型參數:${func(1, 2, a=3, b=4)}
還是之前的例子:
- test:n name: create user which does not existn import_module_functions:n - test.data.custom_functionsn variable_binds:n - TOKEN: debugtalkn - json: {"name": "user", "password": "123456"}n - random: ${gen_random_string(5)}n - authorization: ${gen_md5($TOKEN, $json, $random)}n request:n url: http://127.0.0.1:5000/api/users/1000n method: POSTn headers:n Content-Type: application/jsonn authorization: $authorizationn random: $randomn json: $jsonn validators:n - {"check": "status_code", "comparator": "eq", "expected": 201}n - {"check": "content.success", "comparator": "eq", "expected": true}n
在這裡面有一個variable_binds模塊,之前已經出現過很多次,也一直都沒有講解。但是,本文也不打算進行講解,該部分內容將在下一篇講解參數的定義和引用時再詳細展開。
當前我們只需要知道,在該用例描述中,${gen_random_string(5)}和${gen_md5($TOKEN, $json, $random)}均實現了函數的傳參和調用,而調用的函數正式之前我們定義的gen_random_string和gen_md5。
這裡應該比較好理解,因為函數調用形式與在Python腳本中完全相同。但難點在於,這些描述在YAML/JSON中都是文本字元串形式,ApiTestEngine在載入測試用例的時候,是怎麼識別出函數並完成調用的呢?
具體地,這裡可以拆分為三個需求點:
- 如何在YAML/JSON文本中識別函數?
- 如何將文本字元串的函數拆分為函數名稱和參數?
- 如何使用函數名稱和參數實現對應函數的調用?
正則表達式的妙用
對於第一個需求點,我們之前已經做好了鋪墊,設計了${}作為函數的轉義符;而當初之所以這麼設計,也是為了在載入測試用例時便於解析識別,因為我們可以通過使用正則表達式,非常準確地將函數從文本格式的測試用例中提取出來。
既然Python函數的調用形式是確定的,都是函數名(參數)的形式,那麼使用正則表達式的分組匹配功能,我們就可以很好地實現函數名稱與參數的匹配,也就實現了第二個需求點。
例如,我們可以採用如下正則表達式,來對YAML/JSON中的每一個值(Value)進行匹配性檢查。
r"^${(w+)((.*))}$"nn>>> import ren>>> regex = r"^${(w+)((.*))}$"n>>> string = "${func(3, 5)}"n>>> matched = re.match(regex, string)n>>> matched.group(1)nfuncn>>> matched.group(2)n3, 5n>>>n>>> string = "${func(a=1, b=2)}"n>>> matched = re.match(regex, string)n>>> matched.group(1)nfuncn>>> matched.group(2)na=1, b=2n
可以看出,通過如上正則表達式,如果滿足匹配條件,那麼matched.group(1)就是函數的名稱,matched.group(2)就是函數的參數。
思路是完全可行的,不過我們在匹配參數部分的時候是採用.*的形式,也就是任意字元匹配,匹配的方式不是很嚴謹。考慮到正常的函數參數部分可能使用到的字元,我們可以採用如下更嚴謹的正則表達式。
r"^${(w+)(([$w =,]*))}$"n
這裡限定了五種可能用到的字元,w代表任意字母或數字,= ,代表的是等號、空格和逗號,這些都是參數中可能用到的。而$符號,大家應該還記得,這也是我們設計採用的變數轉義符,$var將不再代表的是普遍的字元串,而是var變數的值。
有了這個基礎,實現如下is_functon函數,就可以判斷某個字元串是否為函數調用。
function_regexp = re.compile(r"^${(w+)(([$w =,]*))}$")nndef is_functon(content):n matched = function_regexp.match(content)n return True if matched else Falsen
不過這裡還有一個問題。通過上面的正則表達式,是可以將函數名稱和參數部分拆分開了,但是在參數部分,還沒法區分具體的參數類型。
例如,在前面的例子中,從${func(3, 5)}解析出來的參數為3, 5,從${func(a=1, b=2)}解析出來的參數為a=1, b=2,我們通過肉眼可以識別出這分別對應著順序參數和字典參數兩種類型,但是程序就沒法自動識別了,畢竟對於程序來說它們都只是字元串而已。
所以,這裡還需要再做一步操作,就是將參數字元串解析為對程序友好的形式。
什麼叫對程序友好的形式呢?這裡就又要用到上一篇文章講到的可變參數和關鍵字參數形式了,也就是func(*args, **kwargs)的形式。
試想,如果我們可以將所有順序參數都轉換為args列表,將所有字典參數都轉換為kwargs字典,那麼對於任意函數類型,我們都可以採用func(*args, **kwargs)的調用形式。
於是,問題就轉換為,如何將參數部分轉換為args和kwargs兩部分。
這就比較簡單了。因為在函數的參數部分,順序參數必須位於字典參數前面,並且以逗號間隔;而字典參數呢,總是以key=value的形式出現,並且也以逗號間隔。
那麼我們就可以利用參數部分的這個特徵,來進行字元串的處理。處理演算法如下:
- 採用逗號作為分隔符將字元串進行拆分;
- 對每一部分進行判斷,如果不包含等號,那麼就是順序參數,將其加入(append)到args列表;
- 如果包含等號,那麼就是字典參數,採用等號作為分隔符進行進一步拆分得到key-value鍵值對,然後再加入到kwargs字典。
對應的Python代碼實現如下:
def parse_function(content):n function_meta = {n "args": [],n "kwargs": {}n }n matched = function_regexp.match(content)n function_meta["func_name"] = matched.group(1)nn args_str = matched.group(2).replace(" ", "")n if args_str == "":n return function_metann args_list = args_str.split(,)n for arg in args_list:n if = in arg:n key, value = arg.split(=)n function_meta["kwargs"][key] = parse_string_value(value)n else:n function_meta["args"].append(parse_string_value(arg))nn return function_metan
可以看出,通過parse_function函數,可以將一個函數調用的字元串轉換為函數的結構體。
例如,${func(1, 2, a=3, b=4)}字元串,經過parse_function轉換後,就可以得到該函數的名稱和參數信息:
function_meta = {n func_name: func,n args: [1, 2],n kwargs: {a:3, b:4}n}n
這也就徹底解決了第二個需求點。
實現函數的調用
在此基礎上,我們再看第三個需求點,如何使用函數名稱和參數實現對應函數的調用,其實也就很簡單了。
在上一篇文章中,我們實現了對函數的定義,並且將所有定義好的函數都添加到了一個字典當中,假如字典名稱為custom_functions_dict,那麼根據以上的函數信息(function_meta),就可以採用如下方式進行調用。
func_name = function_meta[func_name]nargs = function_meta[args]nkwargs = function_meta[kwargs]ncustom_functions_dict[func_name]](*args, **kwargs)n
具體的,在ApiTestEngine中對應的Python代碼片段如下:
def get_eval_value(self, data):n """ evaluate data recursively, each variable in data will be evaluated.n """n if isinstance(data, (list, tuple)):n return [self.get_eval_value(item) for item in data]nn if isinstance(data, dict):n evaluated_data = {}n for key, value in data.items():n evaluated_data[key] = self.get_eval_value(value)nn return evaluated_datann if isinstance(data, (int, float)):n return datann # data is in string format heren data = "" if data is None else data.strip()n if utils.is_variable(data):n # variable marker: $varn variable_name = utils.parse_variable(data)n value = self.testcase_variables_mapping.get(variable_name)n if value is None:n raise exception.ParamsError(n "%s is not defined in bind variables!" % variable_name)n return valuenn elif utils.is_functon(data):n # function marker: ${func(1, 2, a=3, b=4)}n fuction_meta = utils.parse_function(data)n func_name = fuction_meta[func_name]n args = fuction_meta.get(args, [])n kwargs = fuction_meta.get(kwargs, {})n args = self.get_eval_value(args)n kwargs = self.get_eval_value(kwargs)n return self.testcase_config["functions"][func_name](*args, **kwargs)n else:n return datan
這裡還用到了遞歸的概念,當參數是變數(例如gen_md5($TOKEN, $json, $random)),或者為列表、字典等嵌套類型時,也可以實現正常的解析。
總結
到此為止,我們就解決了測試用例(YAML/JSON)中實現Python函數定義和調用的問題。
還記得《探索優雅的測試用例描述方式》末尾提到的用例模板引擎技術實現的三大塊內容么?
- 如何在用例描述(YAML/JSON)中實現函數的定義和調用
- 如何在用例描述中實現參數的定義和引用,包括用例內部和用例集之間
- 如何在用例描述中實現預期結果的描述和測試結果的校驗
第一塊內容總算是講完了,下一篇文章將開始講解如何在用例描述中實現參數的定義和引用的問題。
相關文章
- 《ApiTestEngine 演進之路(2)探索優雅的測試用例描述方式》
- 《ApiTestEngine 演進之路(3)測試用例中實現Python函數的定義》
- ApiTestEngine GitHub源碼
推薦閱讀:
※再談 API 的撰寫 - 總覽
※網站後台要做客戶端API介面,介面文檔如何寫?
※如何讓Scaladoc鏈接到外部API?