Python腳本解析swagger接口文檔自動生成json/excel格式的接口測試用例

背景:公司項目java開發使用swagger工具作爲接口文檔,每次設計接口測試用例的時候,先是設計好excel表頭,然後再一步一步的ctrl+c\ctrl+v很多重複的工作,於是想使用python來解析接口返回的json對象數據,然後清洗重新組成excel新的測試用例,又因爲學習了別人的httprunner接口測試框架,十分友善支持json/yaml格式的接口測試用例,感覺很契合,既然有了思路,然而並沒有急着用python去解析,而是選擇了java,先是模擬了一遍,並沒有完整實現,只導出了部分包含接口地址等接口數據<文末彩蛋>。

再說一下代碼編程的工作者,首先要有編程思維,個人理解就是在做事之前,先要理解需求,需要什麼條件,怎麼做才能滿足,最後纔是爲什麼要這麼做,有沒有更好的方法?我們都知道代碼是可複用的,所以還是百度吧,搜索的結果是出人意料的多:swagger 自動生成接口測試用例\swagger 自動生成接口測試用例\swagger 自動生成接口測試用例,這不是重要的事情要說三遍,而是找到了幾篇很雷同又很同步的python腳本,初步斷定基本能用。

厚顏說一下<拿來主義>在程序開發中是屢見不鮮的事情,但是有一個點值得注意,並不是所有網上收來的代碼都能夠正確在本地執行及一步達到你想要的結果,如果不能還是自己乖乖的寫,雖然效率方面差了一點,自己一步一步的調試出符合當前環境下的結果,需要不斷的優化。前面搜索出來的幾個關於swagger的相關腳本,其中有幾處不適合我的本地環境:

1、excel接口測試用例沒有請求參數,如params;

2、在解析json對象的deprecated描述用法,如果不存在的key直接報錯;

3、在uri--$ref拆分的時候,如果描述過多(多個//符號),可能取值錯誤。

本地執行分析完成後,就開始修改他們的代碼,儘管也花了一定的時間,但是最後整體的代碼是經過優化的,雖然不確定比原來的好,但是一定是適合當前環境下的,或許閒來無事還可以再進一步優化:目前已實現生成json接口測試用例,導出excel表格,增加接口請求參數入列,備份文件、對比接口文件等等功能。

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @File    : swagger.py
"""
導庫順序:優先基礎庫\第三方庫\自定義封裝
格式建議:import一行一個
from導入可以import後面用逗號分隔
"""
import os
import json
import requests
from utils.HandleLogging import log
from utils.HandleJson import write_data
from utils.HandleConfig import HandleConfig
from utils.HandleDirFile import HandleDirFile
from utils.HandleExcel import Write_excel
import config

# 創建可操作配置文件的對象
conf = HandleConfig(config.config_path + "\common.conf")
# 創建可操作目錄及文件的對象
handlefile = HandleDirFile()
# 創建可操作xlsx文件的對象
w = Write_excel(config.xlsCase_path)


class AnalysisSwaggerJson(object):
    """
    swagger自動生成接口測試用例的工具類,此類以生成json格式的測試用例
    """
    
    def __init__(self, url):
        '''
           初始化類,指定請求的swagger接口地址
        '''
        self.url = url
        self.interface = {}  # json接口測試用例類型
        self.case_list = []  # 測試用例的名稱
        self.tags_list = []  # 測試用例的標籤
        # 定義測試用例集格式
        self.http_suite = {"config": {"name": "", "base_url": "", "variables": {}},
                           "testcases": []}
        # 定義測試用例格式
        self.http_testcase = {"name": "", "testcase": "", "variables": {}}
        
        
        # 這些目錄的存放,需要統一規劃,存在多個不同目錄下的文件目錄不方便管理,在執行時容易串門;
        # 需要提示:在請求swagger接口文檔地址的時候,記得去config配置文件下修改對應的路徑
        # 生成api測試用例地址,不存在則創建
        if not os.path.exists(config.case_path):
            os.mkdir(config.case_path)
            
        # 備份文件,如果不存在備份目錄則備份,否則的實現方案在其他方法內
        if not os.path.exists(config.back_path):
            handlefile.copy_dir(config.case_path, config.back_path)
        
        
    def analysis_json_data(self, isDuplicated=False):
        """
                       解析json格式數據的主函數
        :return:
        """
        # swagger接口文檔地址,其中運營後臺的接口地址,請求分模塊,全量或者其他服務菜單
        if "9527" in self.url:
            try:
                res = requests.get(self.url + '/v2/api-docs?group=全量接口').json()  # 這纔是swagger接口請求的地址
                write_data(res, 'data.json')
            except Exception as e:
                log.error('請求swagger地址錯誤. 異常如下: {}'.format(e))
                raise e
        else:
            try:
                res = requests.get(self.url + '/v2/api-docs').json()  # 這纔是swagger接口請求的地址
                write_data(res, 'data.json')
            except Exception as e:
                log.error('請求swagger地址錯誤. 異常如下: {}'.format(e))
                raise e
        
        self.data = res['paths']  # 取接口地址返回的path數據,包括了請求的路徑
        self.basePath = res['basePath']  # 獲取接口的根路徑/hcp
        self.url = 'http://' + res['host']  # 第一錯,swagger文檔是ip地址,使用https協議會錯誤,注意接口地址的請求協議
        self.title = res['info']['title']  # 獲取接口的標題
        self.http_suite['config']['name'] = self.title  # 在初始化用例集字典更新值
        self.http_suite['config']['base_url'] = self.url

        self.definitions = res['definitions']  # body參數
        
        for tag_dict in res['tags']:
            self.tags_list.append(tag_dict['name'])
            
        i = 0
        for tag in self.tags_list:
            self.http_suite['testcases'].append({"name": "", "testcase": "", "variables": {}})
            self.http_suite['testcases'][i]['name'] = tag
            self.http_suite['testcases'][i]['testcase'] = 'testcases/' + tag + '.json'
            i += 1
                
        suite_path = config.testsuites_path
        # 測試用例集目錄不存在,則創建
        if not os.path.exists(suite_path):
            os.makedirs(suite_path)
            
        testsuite_json_path = os.path.join(suite_path, '{}_testsuites.json'.format(self.title))
        # 數據寫入
        write_data(self.http_suite, testsuite_json_path)
                
        if isinstance(self.data, dict):  # 判斷接口返回的paths數據類型是否dict類型
            for tag in self.tags_list:  # 前面已經把接口返回的結果tags分別寫入了tags_list空列表,再從json對應的tag往裏面插入數據
                self.http_case = {"config": {"name": "", "base_url": "", "variables": {}}, "teststeps": []}
                for key, value in self.data.items():
                    for method in list(value.keys()):
                        params = value[method]
                        if not 'deprecated' in value.keys():  # deprecated字段標識:接口是否被棄用,暫時無法判斷,使用consumes偷換
                            if params['tags'][0] == tag:
                                self.http_case['config']['name'] = params['tags'][0]
                                self.http_case['config']['base_url'] = self.url
                                case = self.wash_params(params, key, method, tag)
                                self.http_case['teststeps'].append(case)
                        else:
                            log.info(
                                'interface path: {}, if name: {}, is deprecated.'.format(key, params['operationId']))
                            break
                        
                testcases_path = config.testcases_path
                
                # testcases目錄不存在則創建
                if not os.path.exists(testcases_path):
                    os.makedirs(testcases_path)
                    
                testcase_json_path = os.path.join(testcases_path, tag + '.json')
#                 生成testcase文件
                write_data(self.http_case, testcase_json_path.replace("/", "_"))
        
        else:
            log.error('解析接口數據異常!url 返回值 paths 中不是字典.')
            return 'error'
        
        # 生成完整的json測試用例之後,開始備份接口數據 ,以備作爲接口變更的依據
        if isDuplicated:
            handlefile.copy_dir(config.case_path, config.back_path)


    def wash_params(self, params, api, method, tag):
        """
        清洗數據json,把每個接口數據都加入到一個字典中
        :param params:
        :param params_key:
        :param method:
        :param key:
        :return:
        replace('false', 'False').replace('true', 'True').replace('null','None')
        """
        # 定義接口數據格式
        http_interface = {"name": "", "variables": {},
                          "request": {"url": "", "method": "", "headers": {}, "json": {}, "params": {}}, "validate": [],
                          "output": []}
        # 測試用例的數據格式:
        http_api_testcase = {"name": "", "api": "", "variables": {}, "validate": [], "extract": [], "output": []}
        
        name = params['summary'].replace('/', '_')  # 這裏的問題需要具體來分析,開發有時概要使用其他符號分割
        http_interface['name'] = name
        http_api_testcase['name'] = name
        http_api_testcase['api'] = 'api/{}/{}.json'.format(tag, name)  # 這是寫入testcasejson下的名字,不是生成api的目錄
        http_interface['request']['method'] = method.upper()
        http_interface['request']['url'] = api.replace('{', '$').replace('}', '')  # 這個是替換uri中的/get請求的拼接方式,有些是?參數=&參數拼接,需要另外解析
        parameters = params.get('parameters')  # 未解析的參數字典
        responses = params.get('responses')
        
        if not parameters:  # 確保參數字典存在
            parameters = {}
        # 給測試用例字典,加入解析出來的參數
        for each in parameters:
            if each.get('in') == 'body':  # body 和 query 不會同時出現
                schema = each.get('schema')
                if schema:
                    ref = schema.get('$ref')
                    if ref:
                        param_key = ref.split('/', 2)[-1]  # 這個uri拆分,根據實際情況來取第幾個/反斜槓
                        param = self.definitions[param_key]['properties']
                        for key, value in param.items():
                            if 'example' in value.keys():
                                http_interface['request']['json'].update({key: value['example']})
                            else:
                                http_interface['request']['json'].update({key: ''})
                                
            elif each.get('in') == 'query':
                name = each.get('name')
                for key in each.keys():
                    if not 'example' in key:  # 取反,要把在query的參數寫入json測試用例
                        http_interface['request']['params'].update({name: each[key]})
            
        
        for each in parameters:
            if each.get('in') == 'header':
                name = each.get('name')
                for key in each.keys():
                    if 'example' in key:
                        http_interface['request']['headers'].update({name: each[key]})
                    else:
                        if name == 'token':
                            http_interface['request']['headers'].update({name: '$token'})
                        else:
                            http_interface['request']['headers'].update({name: ''})
                            
                            
        for key, value in responses.items():
            schema = value.get('schema')
            if schema:
                ref = schema.get('$ref')
                if ref:
                    param_key = ref.split('/')[-1]
                    res = self.definitions[param_key]['properties']
                    i = 0
                    for k, v in res.items():
                        if 'example' in v.keys():
                            http_interface['validate'].append({"eq": []})
                            http_interface['validate'][i]['eq'].append('content.' + k)
                            http_interface['validate'][i]['eq'].append(v['example'])
                            http_api_testcase['validate'].append({"eq": []})
                            http_api_testcase['validate'][i]['eq'].append('content.' + k)
                            http_api_testcase['validate'][i]['eq'].append(v['example'])
                            i += 1
                else:
                    if  len(http_interface['validate']) != 1:
                        http_interface['validate'].append({"eq": []})
            else:
                if  len(http_interface['validate']) != 1:
                    http_interface['validate'].append({"eq": []})
        
        # 測試用例的請求參數爲空字典,則刪除這些key
        if http_interface['request']['json'] == {}:
            del http_interface['request']['json']
        
        if http_interface['request']['params'] == {}:
            del http_interface['request']['params']
        
        # 定義接口測試用例
        api_path = config.case_path
        tags_path = os.path.join(api_path, tag).replace("/", "_")
        
        # 創建不存在的文件目錄
        if not os.path.exists(api_path):
            os.mkdir(api_path)

        if not os.path.exists(tags_path):
            os.mkdir(tags_path)
        
        json_path = os.path.join(tags_path, http_interface['name'] + '.json')
        
        write_data(http_interface, json_path)  # 寫入數據

        return http_api_testcase

    
    def write_excel(self, url, filelist):
        '''
           將生成的json格式的數據,轉換成xlsx寫入文件
        '''
        li1 = url.split(":")
        host = li1[1].replace("/", "")
        port = li1[2][:4]
        uri = li1[2][4:]
        count = 1
        caseId = 0
        for file in filelist:
            caseId += 1
            count += 1
            inter_name = file.split("\\")[-2]  # 獲取接口測試用例的上級目錄名稱:組成name-tag的用例title
            with open(file, 'r', encoding='utf-8') as rdfile:
                text = json.load(rdfile)
                title = inter_name + '-' + text['name']
                method = text['request']['method'].upper()
                w.write(count, 1, "apiTest_" + str(caseId))
                w.write(count, 2, title)
                w.write(count, 4, method)
                w.write(count, 5, host)
                w.write(count, 6, port)
                if 'json' in text['request'].keys():  # post請求的接口相關數據寫入excel
                    url = text['request']['url']
                    params = text['request']['json']
                    w.write(count, 7, uri + url)
                    w.write(count, 8, json.dumps(params))
                elif 'params' in text['request'].keys():  # get請求的接口參數寫入
                    url = text['request']['url']
                    jsonp = str(text['request']['params'])
                    join_text = jsonp.replace("{", "").replace("}", "").replace(":", "=").replace("'", "").replace(",", "&").replace(" ", "")
                    w.write(count, 7, uri + url)
                    w.write(count, 8, join_text)
                else:  # 將url中包含$符號的get請求的參數單獨提取出來寫入params
                    url = text['request']['url'].replace('{', '$').replace('}', '')
                    start_index = url.find("$")
                    url1 = url[:start_index]
                    params = url[start_index:]
                    w.write(count, 7, uri + url1)
                    if "$" in params:
                        w.write(count, 8, params)
            

if __name__ == '__main__':
    url = conf.get_value("swaggerUrl", "dev_trade_url")
    js = AnalysisSwaggerJson(url)
    js.analysis_json_data()
#     for i in url.split(","):
#         AnalysisSwaggerJson(i).AnalysisJsonData()
#     js.analysis_json_data()
    js.write_excel(url, handlefile.get_file_list(config.case_path))
#     handlefile.diff_dir_file(config.case_path, config.back_path)

因爲大多數是公司外網地址,隱祕數據基本使用本地讀取配置文件獲取,再次感謝以上源碼的作者,站在先驅的肩膀上前行。

秉承着程序員拿來主義的優良傳統,特此提供本人項目的github,敬請各位看官笑納!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章