自动化测试平台开发(九):接口测试 - YAPI数据同步

前面两篇记录了后端接口实现、前端页面交互实现的一个示例,后面需要做的是把需求的其他后端接口和前端功能实现,期间踩了许多的坑后,基本实现了核心功能,并demo。

本篇介绍 YAPI数据同步的实现。

 

1. 原理

 

2. 源码

 mongo操作

 1 #!/usr/bin/python
 2 # -*- coding:utf-8 _*- 
 3 """
 4 @author:TXU
 5 @file:mongo.py
 6 @time:2021/11/11
 7 @email:tao.xu2008@outlook.com
 8 @description:
 9 """
10 from pymongo import MongoClient
11 from pymongo.errors import ConnectionFailure
12 import unittest
13 
14 from loguru import logger as default_logger
15 from common.singleton import Singleton
16 
17 
18 class MongoDBOperation(object, metaclass=Singleton):
19     """MongoDB数据库操作"""
20 
21     def __init__(self, db_host, db_port, db_user, db_password, db_name,
22                  mechanism='SCRAM-SHA-1', show=False, logger=default_logger):
23         self.db_host = db_host
24         self.db_port = db_port
25         self.db_user = db_user
26         self.db_password = db_password
27         self.db_name = db_name
28         self.mechanism = mechanism
29         self.show = show
30         self.logger = logger
31 
32         self.conn = None
33         self.database = None
34         self.connect()
35 
36     def connect(self):
37         """
38         connect to MangoDB
39         :return:
40         """
41         uri = "mongodb://%s:%s@%s:%s" % (self.db_user, self.db_password, self.db_host, self.db_port)
42         self.logger.info("连接 {} ...".format(uri))
43         self.conn = MongoClient(uri, authSource='admin', authMechanism=self.mechanism)
44         try:
45             self.conn.admin.command('ping')
46             self.logger.info("MangoDB server available")
47         except ConnectionFailure as e:
48             self.logger.error("MangoDB server not available")
49             raise e
50 
51         try:
52             self.database = self.conn[self.db_name]
53             self.logger.info("数据库: %s 连接成功!" % self.db_name)
54         except Exception as e:
55             raise e
56 
57     def close(self):
58         self.logger.info("断开MangoDB数据库的连接...")
59         self.conn.close()
60 
61     def read_table(self, table_name, query_filter=None, projection=None, limit=0):
62         """
63         读取目标表中数据
64         :param table_name: 表名称
65         :param query_filter: 查询条件 {}
66         :param projection: 要读取字段清单 [] / {}, {"_id": False}
67         :param limit: maximum number of results to return. A limit of 0 (the default) is equivalent to setting no limit
68         :return:
69         """
70         self.logger.info("读取目标表:{},query_filter:{}".format(table_name, query_filter))
71         export_table = []
72         try:
73             table = self.database[table_name]
74             for item in table.find(query_filter, projection, limit=limit):
75                 export_table.append(item)
76             self.logger.info("MangoDB表:%s读取完毕,共读取%s行数据!" % (table_name, len(export_table)))
77         except Exception as e:
78             self.logger.error("查询失败或者数据库中无此表!数据表:{},query_filter:{}".format(table_name, query_filter))
79             raise e
80         return export_table
View Code
 
获取yapi info
  1 #!/usr/bin/python
  2 # -*- coding:utf-8 _*- 
  3 """
  4 @author:TXU
  5 @file:get_api_from_mongo.py
  6 @time:2021/11/12
  7 @email:tao.xu2008@outlook.com
  8 @description:
  9 """
 10 import json
 11 
 12 # from apps.api_test import logger
 13 from loguru import logger
 14 from common.mongodb import MongoDBOperation
 15 
 16 
 17 MONGODB_HOST = '192.168.0.1'
 18 MONGODB_PORT = 7211
 19 MONGODB_USER = 'user'
 20 MONGODB_PWD = 'password'
 21 MONGODB_DATABASE = 'yapi'
 22 
 23 
 24 class YapiMongoDB(MongoDBOperation):
 25     """从mongodb数据库获取yapi数据"""
 26     def __init__(self,
 27                  db_host=MONGODB_HOST,
 28                  db_port=MONGODB_PORT,
 29                  db_user=MONGODB_USER,
 30                  db_password=MONGODB_PWD,
 31                  db_name=MONGODB_DATABASE):
 32         super(YapiMongoDB, self).__init__(db_host, db_port, db_user, db_password, db_name, logger=logger)
 33         pass
 34 
 35     def get_projects_by_api_full_path(self, api_full_path):
 36         # 根据full path查找项目: fullpath.startswith(project.basepath)
 37         match_projects = []
 38         projects = self.read_table('project')
 39         if len(projects) == 0:
 40             raise Exception("YAPI中没找到project")
 41         for p in projects:
 42             if api_full_path.startswith(p.get('basepath')):
 43                 match_projects.append(p)
 44         return match_projects
 45 
 46     def get_api_by_method_fullpath(self, method, fullpath):
 47         # 根据full path查找项目: 项目.basepath in fullpath
 48         match_projects = self.get_projects_by_api_full_path(fullpath)
 49         # 排序:basepath长度从大到小:优先检索basepath不为空的项目
 50         for p in sorted(match_projects, key=lambda x: x.get('basepath'), reverse=True):
 51             basepath = p.get('basepath')
 52             p_id = p.get('_id')
 53             path = fullpath.replace(basepath, '')
 54             query_filter = {'project_id': p_id, 'method': method.upper(), 'path': path}
 55             apis = self.read_table('interface', query_filter)
 56             if len(apis) == 0:
 57                 continue
 58             return apis[0]
 59         return None
 60 
 61     def get_api_by_id(self, api_id):
 62         query_filter = {'_id': api_id}
 63         apis = self.read_table('interface', query_filter)
 64         if len(apis) == 0:
 65             raise Exception("YAPI中没找到对应接口, {}".format(query_filter))
 66         return apis[0]
 67 
 68     def get_project_by_id(self, project_id):
 69         query_filter = {'_id': project_id}
 70         projects = self.read_table('project', query_filter)
 71         if len(projects) == 0:
 72             raise Exception("YAPI中没找到对应project:{}".format(query_filter))
 73         return projects[0]
 74 
 75     def get_api_cat_by_id(self, cat_id):
 76         query_filter = {'_id': cat_id}
 77         cats = self.read_table('interface_cat', query_filter)
 78         if len(cats) == 0:
 79             raise Exception("YAPI中没找到对应接口分类:{}".format(query_filter))
 80         return cats[0]
 81 
 82     def pop_data_id(self, data_list):
 83         """
 84         去掉data中ObjectId类型字段,如 '_id': ObjectId('607d377f9f23fd3457e2f1ff')
 85         req_headers | req_query | yapi_req_body_form
 86         :param data_list:
 87         :return:
 88         """
 89         for data in data_list:
 90             data.pop('_id')
 91         return data_list
 92 
 93     def parse_api(self, api):
 94         """
 95         解析yapi原始数据
 96         :param api:
 97         :return:
 98         """
 99         api_name = api.get("title")
100         api_path = api.get("path")
101         if api_path.startswith('/internal'):
102             # logger.warning('{}->{}(内部接口,跳过!!!)'.format(api_name, api_path))
103             raise Exception('{}->{}(内部接口,跳过!!!)'.format(api_name, api_path))
104         api_project_id = api.get("project_id"),
105         api_project = self.get_project_by_id(api_project_id[0])
106         prj_basepath = api_project.get("basepath")
107         req_body_other_src = api.get('req_body_other', '{}')
108         res_body_src = api.get('res_body', '{}')
109         try:
110             req_body_other = json.loads(req_body_other_src)
111         except Exception as e:
112             req_body_other = {"JSONDecodeError": str(e), "body": req_body_other_src}
113         try:
114             res_body = json.loads(api.get('res_body', '{}'))
115         except Exception as e:
116             res_body = {"JSONDecodeError": str(e), "body": res_body_src}
117 
118         api_info = {
119             "name": api_name,
120             "description": api.get("desc"),
121             "yapi_id": api.get("_id"),
122             "yapi_project_id": api.get("project_id"),  # 本地接口表无此字段
123             "yapi_cat_id": api.get("catid"),  # 本地接口表无此字段
124             "method": api.get("method"),
125             "path": prj_basepath + api_path,
126             "yapi_req_headers": self.pop_data_id(api.get('req_headers', [])),
127             "yapi_req_params": self.pop_data_id(api.get('req_params', [])),
128             "yapi_req_query": self.pop_data_id(api.get('req_query', [])),
129             "yapi_req_body_form": self.pop_data_id(api.get('req_body_form', [])),
130             "yapi_req_body_other": req_body_other,
131             "yapi_res_body": res_body
132         }
133         return api_info
134 
135     def get_api_info_by_id(self, yapi_id):
136         """
137         根据yapi id获取yapi接口详情,并解析
138         :param yapi_id:
139         :return:
140         """
141         yapi = self.get_api_by_id(yapi_id)
142         return self.parse_api(yapi)
143 
144     def get_api_info_by_method_path(self, method, api_full_path):
145         """
146         根据api_full_path获取yapi接口详情,并解析
147         :param method: 请求方法
148         :param api_full_path: 请求path,全路径
149         :return:
150         """
151         yapi = self.get_api_by_method_fullpath(method, api_full_path)
152         if not yapi:
153             raise Exception("YAPI中没找到对应接口, {} {}".format(method, api_full_path))
154         return self.parse_api(yapi)
155 
156 
157 if __name__ == '__main__':
158     yapi_db = YapiMongoDB()
159     yapi_info = yapi_db.get_api_by_method_fullpath('GET', '/xxxx/setting/get_setting')
160     print(yapi_info)
View Code

view:

  1 #!/usr/bin/python
  2 # -*- coding:utf-8 _*- 
  3 """
  4 @author:TXU
  5 @file:update.py
  6 @time:2021/11/11
  7 @email:tao.xu2008@outlook.com
  8 @description:
  9 """
 10 import traceback
 11 
 12 from rest_framework.views import APIView
 13 from apps.api_test import logger
 14 from apps.api_test.view_set.base import JsonResponse
 15 from apps.api_test.models import YApiEvent
 16 from apps.api_test.yapi.yapi_sync import APICaseTemplate, YAPIEventDataSync
 17 
 18 
 19 # 用例模板更新:读取接口YAPI定义数据,生成并更新接口请求用例模板
 20 class APICaseTemplateUpdateView(APIView):
 21     """读取接口YAPI定义数据,生成并更新接口请求用例模板"""
 22     def post(self, request, *args, **kwargs):
 23         api_list = request.data
 24         api_temp = APICaseTemplate()
 25         if 'all' in api_list:
 26             res_list = api_temp.update_all()
 27         else:
 28             res_list = api_temp.bulk_update_template(api_list)
 29         response = {"msg": "用例模板更新完成!", "data": res_list}
 30         return JsonResponse(response, status=200)
 31 
 32 
 33 # yapi事件监听:YAPI web hook事件监听,接受事件并插入YAPIEvent表
 34 class YAPIEventMonitorView(APIView):
 35     def post(self, request, *args, **kwargs):
 36         """
 37         YAPI接口变更事件监听处理
 38         1. 客户端更新接口成功后触发
 39             {'_id': 365459, 'YApiEvent': 'yapi_interface_update'}
 40         2. 客户端新增接口成功后触发
 41             TODO
 42         3. 客户端删除接口成功后触发
 43             TODO
 44         :param request:
 45         """
 46         data = request.data
 47         print('data:{}'.format(data))
 48         yapi_id = data.get("_id", 0)
 49         event = data.get("YApiEvent", "null")
 50         exist_events = YApiEvent.objects.filter(yapi_id__exact=yapi_id, event__exact=event)
 51         if exist_events.count() > 0:
 52             response = {
 53                 "msg": "yapi变更事件已存在,忽略",
 54                 "data": data
 55             }
 56         else:
 57             YApiEvent.objects.create(
 58                 **{
 59                     'yapi_id': yapi_id,
 60                     'event': event,
 61                     'content': data,
 62                 }
 63             )
 64             response = {
 65                 "msg": "yapi变更事件添加成功",
 66                 "data": data
 67             }
 68 
 69         return JsonResponse(response, status=200)
 70 
 71 
 72 # yapi数据同步:YAPI变更事件->同步数据到本地接口
 73 class YAPIEventDataSyncView(APIView):
 74     """YAPI变更事件->同步数据到本地接口"""
 75 
 76     def post(self, request, *args, **kwargs):
 77         """
 78         {'list':'all'}  -- data_sync()
 79         {'list':[]}  -- data_sync_single(yapi_event)
 80         :param request:
 81         :param args:
 82         :param kwargs:
 83         :return:
 84         """
 85         yapi_event_list = request.data
 86         yapi_data_sync = YAPIEventDataSync()
 87         if 'all' in yapi_event_list:
 88             res_list = yapi_data_sync.data_sync_all()
 89         else:
 90             res_list = yapi_data_sync.data_sync_bulk(yapi_event_list)
 91         response = {"msg": "更新处理完成!", "data": res_list}
 92 
 93         return JsonResponse(response, status=200)
 94 
 95 
 96 # 接口拉取同步:本地接口主动拉取YAPI数据同步
 97 class APIDataSyncView(APIView):
 98     """本地接口主动拉取YAPI数据同步"""
 99 
100     def post(self, request, *args, **kwargs):
101         api_list = request.data
102         v_yapi_events = []
103         yapi_data_sync = YAPIEventDataSync()
104         for api in api_list:
105             logger.info("更新接口:{} -> {}".format(api.get('id'), api.get('name')))
106             yapi_id = api.get('yapi_id', '')
107             # 构建类YAPIEvent字典,调用YAPIEventDataSync同步数据
108             v_yapi_events.append({
109                 'id': 0,
110                 'yapi_id': yapi_id,
111                 'event': 'pull_yapi_info',
112                 'api_id': api.get('id'),
113                 'method': api.get('method'),
114                 'path': api.get('path'),
115             })
116         res_list = yapi_data_sync.data_sync_bulk(v_yapi_events)
117         response = {"msg": "更新处理完成!", "data": res_list}
118         return JsonResponse(response, status=200)
119 
120 
121 if __name__ == '__main__':
122     pass
View Code

url路由:

 1     # YAPI事件监听
 2     url(r'api/yapi_event/add', YAPIEventMonitorView.as_view()),
 3     # YAPI事件数据同步到本地接口
 4     url(r'api/yapi_event/data_sync', YAPIEventDataSyncView.as_view()),
 5     # 本地接口拉取YAPI数据同步
 6     url(r'api/data_sync', APIDataSyncView.as_view()),
 7     # 本地接口用例模板更新
 8     url(r'api/update/case_template', APICaseTemplateUpdateView.as_view()),
 9     # 接口update_status同步到接口变更历史表
10     url(r'api/sync/update_history/update_status', ApiSyncUpdateStatusToHistoryViewSet.as_view()),
View Code

 

 

-------- THE END --------

posted @ 2021-12-04 17:58  徒手沉浮  阅读(522)  评论(0编辑  收藏  举报