一劈彪、背景
由于目前團隊使用的HTTP接口測試框架是借鑒 HTTPrunner2 的基礎(chǔ)上進行二次開發(fā)的齿诉,組員每次編寫api都是fiddler抓包然后使用har2case去解析生成api用例筝野,然后再編寫修改≡辆纾基本都做著一些重復(fù)的工作遗座,而開發(fā)團隊之前的接口文檔管理方式比較雜亂(不同的項目組存在不同的接口管理方式),剛好近期前端的同學(xué)開始整合接口文檔平臺俊扳,決定統(tǒng)一使用swagger來進行管理途蒋。那么借著這個契機,就可以嘗試解析swagger自動生成用例馋记,來降低重復(fù)編寫api的工作号坡,只需要關(guān)注testcase場景的串聯(lián)即可。為后續(xù)需求實例化默默的打下基礎(chǔ)梯醒。
二宽堆、swagger 分析
記得有這個想法的時候,就想著直接F12抓包看下接口看下每個api數(shù)據(jù)的結(jié)構(gòu)來進行分析茸习,然后直接 懵逼 畜隶,返回的是個html。沒辦法号胚,百度了一遍籽慢,剛好發(fā)現(xiàn)有人跟我一樣的情況,正好解決了這個問題猫胁,原來swagger文檔的數(shù)據(jù)是在 api-docs 這個接口返回的json中箱亿,既然數(shù)據(jù)有了,那么接下來 好戲 開場弃秆。
1.swagger 基本結(jié)構(gòu) 參考官網(wǎng)
{
"swagger": "2.0",
"info": "文檔信息相關(guān)",
"host": "127.0.0.1",
"basePath": "/useradmin/user",
"tags": [],
"paths": {},
"definitions": {},
"securityDefinitions": {}
}
首先分析一下swagger的基本結(jié)構(gòu)届惋,其中本次解析的重點對象就是paths和definitions,具體可以去參考swagger官網(wǎng)的描述菠赚。下面對paths和definitions的節(jié)本結(jié)構(gòu)進行逐個分析脑豹。
# paths 節(jié)點
{
"paths":{
"/userAadmin/user/userId":{ # 接口路徑
"get":{ # 請求方法
"tags":[
"用戶管理" # 模塊名稱
],
"summary":"根據(jù)用戶ID獲取信息", # 接口描述
"operationId":"userIdGET",
"consumes":[
"application/json" # 請求參數(shù)類型
],
"produces":[
"*/*"
],
"parameters":[ # 請求參數(shù),注意是 list
{
"name":"userId", # 字段名稱
"in":"query", # 參數(shù)位置 query in params || body in data||json
"description":"商品ID",
"required":true, # 是否必填
"type":"integer",
"format":"int64"
}
],
"responses":{
"200":{
"description":"OK",
"schema":{
"$ref":"#/definitions/UserVo"
}
}
}
}
},
"/userAadmin/user/userUpdate":{ # 接口路徑
"post":{ # 請求方法
"tags":[
"用戶管理" # 模塊名稱
],
"summary":"更新用戶信息", # 接口描述
"operationId":"userUpdatePOST",
"consumes":[
"application/json" # 請求參數(shù)類型
],
"produces":[
"*/*"
],
"parameters":[ # 請求參數(shù)衡查,注意是 list
{
"in":"query", # 參數(shù)位置 query in params || body in data||json
"name":"userId", # 字段名稱
"description":"商品ID",
"required":true, # 是否必填
"type":"integer",
"format":"int64"
}瘩欺,
{
"in":"body", # 參數(shù)位置 query in params || body in data||json
"name":"user", # 對象名稱,首字母小寫峡捡,需要解析的時候轉(zhuǎn)一下
"description":"params description",
"required":true, # 是否必填
"schema":{
"$ref":"#/definitions/User" # 入?yún)ο蠡魍耄枰?definitions 節(jié)點下獲取對應(yīng)的對象字段
}
}
],
"responses":{
"200":{
"description":"OK",
"schema":{
"$ref":"#/definitions/UserVo"
}
}
}
}
}
}
}
從上面的基本結(jié)構(gòu)來看筑悴,paths節(jié)點下面是多個接口信息们拙,示例接口下面描述的是請求方式分別有g(shù)et稍途、post兩種請求接口,在解析的時候需要重點關(guān)注有注釋的節(jié)點砚婆,注意這里的注釋說明 很重要P蹬摹!装盯!
這些節(jié)點是獲取數(shù)據(jù)然后轉(zhuǎn)化接口用例的必需字段坷虑,都是一一對應(yīng)的。
注意:由于開發(fā)維護的接口文檔如果不規(guī)范埂奈,或者亂定義迄损,就會存在解析這些字段的時候存在缺失和錯誤的情況。如果想避免這些情況账磺,就必須事先同相關(guān)開發(fā)統(tǒng)一規(guī)范芹敌。
# definitions 節(jié)點
{
"definitions":{
"UserVo":{ # 對象
"type":"object",
"properties":{ # 屬性字段集合
"userId":{ # 字段
"type":"string",
"example":"10086",
"description":"用戶ID"
},
"userName":{ # 字段
"type":"integer",
"format":"int64",
"example":1,
"description":"用戶名"
},
"age":{ # 字段
"type":"integer",
"format":"int64",
"example":18,
"description":"年齡"
},
"address":{ # 字段
"type":"string",
"example":"深圳市/南山區(qū)/西麗",
"description":"地址信息"
}
}
}
}
}
definitions 節(jié)點獲取的數(shù)據(jù)主要是從paths那邊拿到入?yún)ο髞砥ヅ洌缓蟊闅v獲取 properties 下面的所有keys垮抗,當(dāng)做接口用例的請求參數(shù)氏捞。一般作用于post請求方式的接口。
三冒版、代碼實現(xiàn)
1.在了解了swagger的基本結(jié)構(gòu)之后液茎,那么在代碼實現(xiàn)之前先進行邏輯拆分:
- 先請求接口文檔獲取 entry json
- 從 entry_json 分別獲取 paths / definitions 數(shù)據(jù)
- 從 paths中獲取path對應(yīng)接口模板中的 url
- 從 paths 中獲取 get/post 對應(yīng)模板中request.method
- 從 paths 路徑下獲取請求數(shù)據(jù)類型 consumes
- 從 paths 路徑下獲取請求頭參數(shù) params,根據(jù) in=query 來判斷
- 從 paths 路徑下獲取請求 body 參數(shù)辞嗡,根據(jù) in=body 來判斷
- 獲取響應(yīng)數(shù)據(jù)定義斷言
- 根據(jù)用例模板聚合生成用例文件
# 用例模板
{
"config": {
"base_url": "${ENV(BASE_URL)}",
"name": "testCase description",
"variables": {}
},
"testSteps": testStep_dict
}
testStep_dict = {
"name": "",
"request": {},
"validate": []
}
2.代碼實現(xiàn): doc2case
from loguru import logger
import requests
from kernel.utils import get_target_value, get_file_path, dump_yaml, dump_json
from kernel import USER_AGENT
class SwaggerParser(object):
s = requests.session()
def __init__(self, url):
self.api_doc_json = self.get_entry_json(url)
def get_entry_json(self, url):
response = self.s.get(url).json()
if response is not None:
return response
@staticmethod
def _make_request_url(testStep_dict, path):
testStep_dict["name"] = path
testStep_dict["request"]["url"] = path
@staticmethod
def _make_request_method(testStep_dict, entry_json):
""" parse HAR entry request method, and make testStep method.
"""
testStep_dict["request"]["method"] = [x for x in entry_json.keys()][0].upper()
@staticmethod
def _make_request_headers(testStep_dict, entry_json):
testStep_headers = {}
for method, params in entry_json.items():
testStep_headers["Content-Type"] = params["consumes"][0]
if testStep_headers:
testStep_headers["User-Agent"] = USER_AGENT
testStep_dict["request"]["headers"] = testStep_headers
@staticmethod
def _make_request_params(testStep_dict, entry_json):
for method, params in entry_json.items():
query_dict = {}
for param in params.get("parameters"):
if param.get("in") == "query":
queryString = param.get("name")
if queryString:
query_dict[f"{queryString}"] = f"${queryString}"
testStep_dict["request"]["params"] = query_dict
def _make_request_data(self, testStep_dict, entry_json):
for method, params in entry_json.items():
request_data_key = "json" if params.get("consumes")[0].startswith("application/json") else "data"
if method.upper() in ["POST", "PUT", "PATCH"]:
for param in params.get("parameters"):
if param.get("in") == "body":
schema_obj = param.get("name")
for obj, properties in self.api_doc_json.get("definitions").items():
data_dict = {}
if obj in schema_obj:
for k, v in properties.get("properties").items():
data_dict[k] = f"${k}"
testStep_dict["request"][request_data_key] = data_dict
@staticmethod
def _make_validate(testStep_dict):
testStep_dict["validate"].append({"eq": ["status_code", 200]})
def _prepare_testStep(self, path, entry_json):
testStep_dict = {
"name": "",
"request": {},
"validate": []
}
self._make_request_url(testStep_dict, path)
self._make_request_method(testStep_dict, entry_json)
self._make_request_headers(testStep_dict, entry_json)
self._make_request_params(testStep_dict, entry_json)
self._make_request_data(testStep_dict, entry_json)
self._make_validate(testStep_dict)
return testStep_dict
@staticmethod
def _prepare_config():
return {
"base_url": "${ENV(BASE_URL)}",
"name": "testCase description",
"variables": {}
}
def _prepare_testSteps(self, path, entry_json):
""" make testStep list.
testSteps list are parsed from HAR log entries list.
"""
return [self._prepare_testStep(path, entry_json)]
def _make_testCase(self, path, entry_json):
""" Extract info from HAR file and prepare for testCase
"""
logger.debug("Extract info from HAR file and prepare for testCase.")
config = self._prepare_config()
testSteps = self._prepare_testSteps(path, entry_json)
return {
"config": config,
"testSteps": testSteps
}
def gen_testCase(self, path=None, file_type="yml"):
"""
Generate test cases based on the specified path
"""
if path is not None:
for test_mapping in get_target_value(path, self.api_doc_json.get("paths")):
logger.info(f"Start to generate testCase.: {path}")
testCase = self._make_testCase(path, test_mapping)
file = get_file_path(path, test_mapping) + "." + file_type
dump_yaml(testCase, file) if file_type == "yml" else dump_json(testCase, file)
logger.debug("prepared testCase: {}".format(testCase))
else:
for path, test_mapping in self.api_doc_json.get("paths").items():
logger.info(f"Start to generate testCase.: {path}")
testCase = self._make_testCase(path, test_mapping)
file = get_file_path(path, test_mapping) + "." + file_type
logger.debug("spanned file : {}".format(file))
dump_yaml(testCase, file) if file_type == "yml" else dump_json(testCase, file)
logger.debug("prepared testCase: {}".format(testCase))
以上 doc2case 的代碼封裝是根據(jù)上面拆分的邏輯步驟一步步進行組裝模板數(shù)據(jù)捆等,最后生成的用例文件。默認是在當(dāng)前根目錄下創(chuàng)建api/模塊名/用例名续室。
注意點:以上關(guān)鍵節(jié)點的字段需要根據(jù)實際swagger節(jié)點的字段進行分析楚里,再進行代碼解析。因為不確定開發(fā)是如何去維護對應(yīng)字段猎贴。重點就是要統(tǒng)一字段的命名和格式班缎。
3.案例演示
下面我是使用 setup.py 封裝到命令行執(zhí)行(如果想學(xué)習(xí)如何封裝命令行工具或者打包插件發(fā)布分享,可以關(guān)注一下我其他的文章)
Python 實現(xiàn)命令行工具
無處安放的Py插件她渴,如何正確使用setup.py打包發(fā)布
從圖1 help 中可以看出达址,目前是支持json和yaml兩種格式的用例生成,默認是 json趁耗。只需要傳入 api-doc 的url沉唠,這個是必傳的。也可以根據(jù)指定的path進行單個用例的解析
doc2case url -f yml
從清晰的log中看到此時用例均已生成苛败,這里使用的是loguru 日志满葛,想學(xué)習(xí)了解的可以看下我上面一篇 Python 如何更優(yōu)雅的記錄日志(loguru) 有專門的介紹使用径簿。
圖中左邊的api目錄就是本次生成的用例了,下面是核心代碼的結(jié)構(gòu)嘀韧,右邊的是yaml格式的用例篇亭。是參考HTTPrunner2格式生成的,與har2case 解析的結(jié)果一致锄贷,當(dāng)然本質(zhì)代碼就是參考它的译蒂,只不過是解析的對象不一樣,稍做了修改谊却。
最后柔昼,如果對doc2case工具感興趣想要獲取源碼的,可以 點贊+關(guān)注+回復(fù)炎辨!
代碼已上傳到 pypi 倉庫中可以使用pip直接下載:>> pip install doc2case