├── .gitignore ├── README.md ├── constants.py ├── core ├── __init__.py ├── excel.py ├── log.py ├── mysql.py └── request.py ├── function ├── __init__.py └── func.py ├── requirements ├── run.py └── test.xlsx /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/** 2 | .cache/** 3 | core/.cache/** 4 | *.pyc 5 | core/*.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # api4excel - 接口自动化测试excel篇 2 | 3 | #### 工作原理: 测试用例在excel上编辑,使用第三方库xlrd,读取表格sheet和内容,sheetName对应模块名,Jenkins集成服务发现服务moduleName查找对应表单,运用第三方库requests请求接口,根据结果和期望值进行断言,根据输出报告判断接口测试是否通过。 4 | 5 | ###### 1. 数据准备 6 | - 数据插入(容易实现的测试场景下所需外部数据) 7 | - 准备sql (接口需要重复使用,参数一定得是变量) 8 | 9 | ###### 2.集成部署(运维相关了解即可) 10 | - 平滑升级验证脚本加入自动化 11 | 12 | ###### 3.自动化框架实现 13 | - 调用mysql 14 | - excel遍历测试用例 15 | - requests实现接口调用 16 | - 根据接口返回的code值和Excel对比 17 | - 报告反馈 18 | - 暴露服务 19 | 20 | **写一个简单登录的接口自动化测试** 21 | 22 | ##### 代码的分层如下图: 23 | 24 | ![代码结构](https://upload-images.jianshu.io/upload_images/2955280-61684f7211311214.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 25 | 26 | 27 | #### 一、写一个封装的获取excel表格的模块 #### 28 | 29 | 30 | ![excel.png](http://upload-images.jianshu.io/upload_images/2955280-932a018763266a33.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 31 | 32 | 33 | 代码实现如下: 34 | ``` python 35 | # !/usr/bin/python 36 | # -*- coding: UTF-8 -*- 37 | # author: 赫本z 38 | # 基础包: excel的封装 39 | 40 | import xlrd 41 | workbook = None 42 | 43 | def open_excel(path): 44 | """ 45 | 打开excel 46 | :param path: 打开excel文件的位置 47 | """ 48 | global workbook 49 | if (workbook == None): 50 | workbook = xlrd.open_workbook(path, on_demand=True) 51 | 52 | def get_sheet(sheetName): 53 | """ 54 | 获取页名 55 | :param sheetName: 页名 56 | :return: workbook 57 | """ 58 | global workbook 59 | return workbook.sheet_by_name(sheetName) 60 | 61 | def get_rows(sheet): 62 | """ 63 | 获取行号 64 | :param sheet: sheet 65 | :return: 行数 66 | """ 67 | return sheet.nrows 68 | 69 | def get_content(sheet, row, col): 70 | """ 71 | 获取表格中内容 72 | :param sheet: sheet 73 | :param row: 行 74 | :param col: 列 75 | :return: 76 | """ 77 | return sheet.cell(row, col).value 78 | 79 | def release(path): 80 | """释放excel减少内存""" 81 | global workbook 82 | workbook.release_resources() 83 | del workbook 84 | # todo:没有验证是否可用 85 | 86 | ``` 87 | 88 | 代码封装后当成模块引用,这还是最开始呢。 89 | 90 | #### 二、引用log模块获取日志 #### 91 | 92 | 准备工作: 93 | 需要一个日志的捕获,包括框架和源码抛出的expection。 94 | 代码如下: 95 | ``` python 96 | #!/usr/bin/python 97 | # -*- coding: UTF-8 -*- 98 | # author: 赫本z 99 | # 基础包: 日志服务 100 | import logging 101 | 102 | def get_logger(): 103 | global logPath 104 | try: 105 | logPath 106 | except NameError: 107 | logPath = "" 108 | FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 109 | logging.basicConfig(level=logging.INFO, format=FORMAT) 110 | return logging 111 | ``` 112 | 113 | #### 三、引用requests模块接口测试 #### 114 | 115 | 准备工作: 116 | 需要的请求类型和执行测试的方法。 117 | 代码如下: 118 | 119 | ``` python 120 | #!/usr/bin/python 121 | #-*- coding: UTF-8 -*- 122 | # 基础包:接口测试的封装 123 | 124 | import requests 125 | import core.log as log 126 | import json 127 | 128 | logging = log.get_logger() 129 | 130 | def change_type(value): 131 | """ 132 | 对dict类型进行中文识别 133 | :param value: 传的数据值 134 | :return: 转码后的值 135 | """ 136 | try: 137 | if isinstance(eval(value), str): 138 | return value 139 | if isinstance(eval(value), dict): 140 | result = eval(json.dumps(value)) 141 | return result 142 | except Exception, e: 143 | logging.error("类型问题 %s", e) 144 | 145 | 146 | def api(method, url, data ,headers): 147 | """ 148 | 自定义一个接口测试的方法 149 | :param method: 请求类型 150 | :param url: 地址 151 | :param data: 数据 152 | :param headers: 请求头 153 | :return: code码 154 | """ 155 | global results 156 | try: 157 | if method == ("post" or "POST"): 158 | results = requests.post(url, data, headers=headers) 159 | if method == ("get" or "GET"): 160 | results = requests.get(url, data, headers=headers) 161 | # if method == "put": 162 | # results = requests.put(url, data, headers=headers) 163 | # if method == "delete": 164 | # results = requests.delete(url, headers=headers) 165 | # if method == "patch": 166 | # results == requests.patch(url, data, headers=headers) 167 | # if method == "options": 168 | # results == requests.options(url, headers=headers) 169 | response = results.json() 170 | code = response.get("code") 171 | return code 172 | except Exception, e: 173 | logging.error("service is error", e) 174 | 175 | 176 | def content(method, url, data, headers): 177 | """ 178 | 请求response自己可以自定义检查结果 179 | :param method: 请求类型 180 | :param url: 请求地址 181 | :param data: 请求参数 182 | :param headers: 请求headers 183 | :return: message信息 184 | """ 185 | global results 186 | try: 187 | if method == ("post" or "POST"): 188 | results = requests.post(url, data, headers=headers) 189 | if method == ("get" or "GET"): 190 | results = requests.get(url, data, headers=headers) 191 | if method == ("put" or "PUT"): 192 | results = requests.put(url, data, headers=headers) 193 | if method == ("patch" or "PATCH"): 194 | results = requests.patch(url, data, headers=headers) 195 | response = results.json() 196 | message = response.get("message") 197 | result = response.get("result") 198 | content = {"message": message, "result": result} 199 | return content 200 | except Exception, e: 201 | logging.error("请求失败 %s" % e) 202 | 203 | ``` 204 | 205 | #### 四、关于function模块 #### 206 | 主要调用二次封装的代码,结合业务做一个通用代码。如下: 207 | 208 | ``` python 209 | #!/usr/bin/python 210 | # -*- coding: UTF-8 -*- 211 | # 业务包:通用函数 212 | 213 | 214 | import core.mysql as mysql 215 | import core.log as log 216 | import core.request as request 217 | import core.excel as excel 218 | import constants as cs 219 | from prettytable import PrettyTable 220 | 221 | logging = log.get_logger() 222 | 223 | 224 | class ApiTest: 225 | """接口测试业务类""" 226 | filename = cs.FILE_NAME 227 | 228 | def __init__(self): 229 | pass 230 | 231 | def prepare_data(self, host, user, password, db, sql): 232 | """数据准备,添加测试数据""" 233 | mysql.connect(host, user, password, db) 234 | res = mysql.execute(sql) 235 | mysql.close() 236 | logging.info("Run sql: the row number affected is %s", res) 237 | return res 238 | 239 | def get_excel_sheet(self, path, module): 240 | """依据模块名获取sheet""" 241 | excel.open_excel(path) 242 | return excel.get_sheet(module) 243 | 244 | def get_prepare_sql(self, sheet): 245 | """获取预执行SQL""" 246 | return excel.get_content(sheet, cs.SQL_ROW, cs.SQL_COL) 247 | 248 | def run_test(self, sheet, url): 249 | """再执行测试用例""" 250 | rows = excel.get_rows(sheet) 251 | fail = 0 252 | for i in range(2, rows): 253 | testNumber = str(int(excel.get_content(sheet, i, cs.CASE_NUMBER))) 254 | testData = excel.get_content(sheet, i, cs.CASE_DATA) 255 | testName = excel.get_content(sheet, i, cs.CASE_NAME) 256 | testUrl = excel.get_content(sheet, i, cs.CASE_URL) 257 | testUrl = url + testUrl 258 | testMethod = excel.get_content(sheet, i, cs.CASE_METHOD) 259 | testHeaders = eval(excel.get_content(sheet, i, cs.CASE_HEADERS)) 260 | testCode = excel.get_content(sheet, i, cs.CASE_CODE) 261 | actualCode = request.api(testMethod, testUrl, testData, testHeaders) 262 | expectCode = str(int(testCode)) 263 | failResults = PrettyTable(["Number", "Method", "Url", "Data", "ActualCode", "ExpectCode"]) 264 | failResults.align["Number"] = "l" 265 | failResults.padding_width = 1 266 | failResults.add_row([testNumber, testMethod, testUrl, testData, actualCode, expectCode]) 267 | 268 | if actualCode != expectCode: 269 | logging.info("FailCase %s", testName) 270 | print "FailureInfo" 271 | print failResults 272 | fail += 1 273 | else: 274 | logging.info("Number %s", testNumber) 275 | logging.info("TrueCase %s", testName) 276 | if fail > 0: 277 | return False 278 | return True 279 | ``` 280 | 281 | #### 五、关于参数中constans模块 #### 282 | 283 | 准备工作: 284 | 所有的参数和常量我们会整理到这个文件中,因为设计业务和服务密码、数据库密码这里展示一部分。 285 | 代码如下: 286 | 287 | ``` python 288 | #!/usr/bin/python 289 | # -*- coding: UTF-8 -*- 290 | # 通用包:常量 291 | 292 | CASE_NUMBER = 0 # 用例编号 293 | CASE_NAME = 1 # 用例名称 294 | CASE_DATA = 2 # 用例参数 295 | CASE_URL = 3 # 用例接口地址 296 | CASE_METHOD = 4 # 用例请求类型 297 | CASE_CODE = 5 # 用例code 298 | CASE_HEADERS = 6 # 用例headers 299 | 300 | SQL_ROW = 0 # 预执行SQL的行号 301 | SQL_COL = 1 # 预执行SQL的列号 302 | 303 | FILE_NAME = 'test.xlsx' 304 | ``` 305 | 306 | #### 六、写一个run文件:只是用来执行的,业务和代码剥离。 #### 307 | 308 | 代码如下: 309 | ``` python 310 | #!/usr/bin/python 311 | # -*- coding: UTF-8 -*- 312 | # 验证包:接口测试脚本 313 | 314 | import core.log as log 315 | from function.func import ApiTest 316 | 317 | func = ApiTest() 318 | logging = log.get_logger() 319 | 320 | """1.外部输入参数""" 321 | 322 | module = 'user' 323 | url = 'http://127.0.0.1:8080' 324 | 325 | """2.根据module获取Sheet""" 326 | logging.info("-------------- Execute TestCases ---------------") 327 | sheet = func.get_excel_sheet(func.filename, module) 328 | 329 | # """3.数据准备""" 330 | # logging.info("-------------- Prepare data through MysqlDB --------------") 331 | # sql = func.get_prepare_sql(sheet) 332 | # func.prepare_data(host=host, user=user, password=password, db=db, sql=sql) 333 | 334 | """4.执行测试用例""" 335 | res = func.run_test(sheet, url) 336 | logging.info("-------------- Get the result ------------ %s", res) 337 | 338 | ``` 339 | 340 | 341 | #### 七、查看测试报告(部署到jenkins会通过控制台查看) #### 342 | 343 | 344 | ![报告.png](http://upload-images.jianshu.io/upload_images/2955280-59a60c84bc40b3b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 345 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # author: 赫本z 4 | # 通用包:常量 5 | 6 | 7 | CASE_NUMBER = 0 # 用例编号 8 | CASE_NAME = 1 # 用例名称 9 | CASE_DATA = 2 # 用例参数 10 | CASE_URL = 3 # 用例接口地址 11 | CASE_METHOD = 4 # 用例请求类型 12 | CASE_CODE = 5 # 用例code 13 | CASE_HEADERS = 6 # 用例headers 14 | 15 | SQL_ROW = 0 # 预执行SQL的行号 16 | SQL_COL = 1 # 预执行SQL的列号 17 | 18 | FILE_NAME = 'test.xlsx' -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F1And/api4excel/d60cc7f1f764157fd7805fed66b9fc22d8bdaf7e/core/__init__.py -------------------------------------------------------------------------------- /core/excel.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # author: 赫本z 4 | # 基础包: excel的封装 5 | 6 | import xlrd 7 | workbook = None 8 | 9 | def open_excel(path): 10 | """ 11 | 打开excel 12 | :param path: 打开excel文件的位置 13 | """ 14 | global workbook 15 | if (workbook == None): 16 | workbook = xlrd.open_workbook(path, on_demand=True) 17 | 18 | def get_sheet(sheetName): 19 | """ 20 | 获取页名 21 | :param sheetName: 页名 22 | :return: workbook 23 | """ 24 | global workbook 25 | return workbook.sheet_by_name(sheetName) 26 | 27 | def get_rows(sheet): 28 | """ 29 | 获取行号 30 | :param sheet: sheet 31 | :return: 行数 32 | """ 33 | return sheet.nrows 34 | 35 | def get_content(sheet, row, col): 36 | """ 37 | 获取表格中内容 38 | :param sheet: sheet 39 | :param row: 行 40 | :param col: 列 41 | :return: 42 | """ 43 | return sheet.cell(row, col).value 44 | 45 | def release(path): 46 | """释放excel减少内存""" 47 | global workbook 48 | workbook.release_resources() 49 | del workbook 50 | # todo:没有验证是否可用 -------------------------------------------------------------------------------- /core/log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # author: 赫本z 4 | # 基础包: 日志服务 5 | import logging 6 | 7 | def get_logger(): 8 | global logPath 9 | try: 10 | logPath 11 | except NameError: 12 | logPath = "" 13 | FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 14 | logging.basicConfig(level=logging.INFO, format=FORMAT) 15 | return logging -------------------------------------------------------------------------------- /core/mysql.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # author: 赫本z 4 | # 基础包: MySQL 5 | 6 | import pymysql.cursors 7 | import core.log as log 8 | 9 | 10 | logging = log.get_logger() 11 | conn = None 12 | 13 | def connect(host, user, password, db, charset='utf8'): 14 | """ 15 | 链接Mysql 16 | :param host: 地址 17 | :param user: 用户 18 | :param password: 密码 19 | :param db: 数据库名 20 | :param charset: 数据类型 21 | :return: 链接 22 | """ 23 | global conn 24 | if conn == None: 25 | conn = pymysql.connect(host=host, 26 | user=user, 27 | password=password, 28 | db=db, 29 | charset=charset, 30 | cursorclass=pymysql.cursors.DictCursor) 31 | return conn 32 | 33 | 34 | def execute(sql): 35 | """ 36 | 执行SQL 37 | :param sql: 执行的SQL 38 | :return: 影响行数 39 | """ 40 | global conn 41 | try: 42 | with conn.cursor() as cursor: 43 | res = cursor.execute(sql) 44 | conn.commit() 45 | # 这里一定要写commit 不然提交的sql 都会被事务回滚 46 | return res 47 | except Exception, e: 48 | logging.error("sql is empty or error %s" % e) 49 | 50 | 51 | def close(): 52 | """ 53 | 关闭MySQL连接 54 | :return: None 55 | """ 56 | global conn 57 | conn.close() 58 | -------------------------------------------------------------------------------- /core/request.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*- coding: UTF-8 -*- 3 | # author: 赫本z 4 | # 基础包:接口测试的封装 5 | 6 | import requests 7 | import core.log as log 8 | import json 9 | 10 | logging = log.get_logger() 11 | 12 | def change_type(value): 13 | """ 14 | 对dict类型进行中文识别 15 | :param value: 传的数据值 16 | :return: 转码后的值 17 | """ 18 | try: 19 | if isinstance(eval(value), str): 20 | return value 21 | if isinstance(eval(value), dict): 22 | result = eval(json.dumps(value)) 23 | return result 24 | except Exception, e: 25 | logging.error("类型问题 %s", e) 26 | 27 | 28 | def api(method, url, data ,headers): 29 | """ 30 | 自定义一个接口测试的方法 31 | :param method: 请求类型 32 | :param url: 地址 33 | :param data: 数据 34 | :param headers: 请求头 35 | :return: code码 36 | """ 37 | global results 38 | try: 39 | if method == ("post" or "POST"): 40 | results = requests.post(url, data, headers=headers) 41 | if method == ("get" or "GET"): 42 | results = requests.get(url, data, headers=headers) 43 | # if method == "put": 44 | # results = requests.put(url, data, headers=headers) 45 | # if method == "delete": 46 | # results = requests.delete(url, headers=headers) 47 | # if method == "patch": 48 | # results == requests.patch(url, data, headers=headers) 49 | # if method == "options": 50 | # results == requests.options(url, headers=headers) 51 | response = results.json() 52 | code = response.get("code") 53 | return code 54 | except Exception, e: 55 | logging.error("service is error", e) 56 | 57 | 58 | def content(method, url, data, headers): 59 | """ 60 | 请求response自己可以自定义检查结果 61 | :param method: 请求类型 62 | :param url: 请求地址 63 | :param data: 请求参数 64 | :param headers: 请求headers 65 | :return: message信息 66 | """ 67 | global results 68 | try: 69 | if method == ("post" or "POST"): 70 | results = requests.post(url, data, headers=headers) 71 | if method == ("get" or "GET"): 72 | results = requests.get(url, data, headers=headers) 73 | if method == ("put" or "PUT"): 74 | results = requests.put(url, data, headers=headers) 75 | if method == ("patch" or "PATCH"): 76 | results = requests.patch(url, data, headers=headers) 77 | response = results.json() 78 | message = response.get("message") 79 | result = response.get("result") 80 | content = {"message": message, "result": result} 81 | return content 82 | except Exception, e: 83 | logging.error("请求失败 %s" % e) 84 | -------------------------------------------------------------------------------- /function/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F1And/api4excel/d60cc7f1f764157fd7805fed66b9fc22d8bdaf7e/function/__init__.py -------------------------------------------------------------------------------- /function/func.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # author: 赫本z 4 | # 业务包:通用函数 5 | 6 | 7 | import core.mysql as mysql 8 | import core.log as log 9 | import core.request as request 10 | import core.excel as excel 11 | import constants as cs 12 | from prettytable import PrettyTable 13 | 14 | logging = log.get_logger() 15 | 16 | 17 | class ApiTest: 18 | """接口测试业务类""" 19 | filename = cs.FILE_NAME 20 | 21 | def __init__(self): 22 | pass 23 | 24 | def prepare_data(self, host, user, password, db, sql): 25 | """数据准备,添加测试数据""" 26 | mysql.connect(host, user, password, db) 27 | res = mysql.execute(sql) 28 | mysql.close() 29 | logging.info("Run sql: the row number affected is %s", res) 30 | return res 31 | 32 | def get_excel_sheet(self, path, module): 33 | """依据模块名获取sheet""" 34 | excel.open_excel(path) 35 | return excel.get_sheet(module) 36 | 37 | def get_prepare_sql(self, sheet): 38 | """获取预执行SQL""" 39 | return excel.get_content(sheet, cs.SQL_ROW, cs.SQL_COL) 40 | 41 | def run_test(self, sheet, url): 42 | """再执行测试用例""" 43 | rows = excel.get_rows(sheet) 44 | fail = 0 45 | for i in range(2, rows): 46 | testNumber = str(int(excel.get_content(sheet, i, cs.CASE_NUMBER))) 47 | testData = excel.get_content(sheet, i, cs.CASE_DATA) 48 | testName = excel.get_content(sheet, i, cs.CASE_NAME) 49 | testUrl = excel.get_content(sheet, i, cs.CASE_URL) 50 | testUrl = url + testUrl 51 | testMethod = excel.get_content(sheet, i, cs.CASE_METHOD) 52 | testHeaders = eval(excel.get_content(sheet, i, cs.CASE_HEADERS)) 53 | testCode = excel.get_content(sheet, i, cs.CASE_CODE) 54 | actualCode = request.api(testMethod, testUrl, testData, testHeaders) 55 | expectCode = str(int(testCode)) 56 | failResults = PrettyTable(["Number", "Method", "Url", "Data", "ActualCode", "ExpectCode"]) 57 | failResults.align["Number"] = "l" 58 | failResults.padding_width = 1 59 | failResults.add_row([testNumber, testMethod, testUrl, testData, actualCode, expectCode]) 60 | 61 | if actualCode != expectCode: 62 | logging.info("FailCase %s", testName) 63 | print "FailureInfo" 64 | print failResults 65 | fail += 1 66 | else: 67 | logging.info("Number %s", testNumber) 68 | logging.info("TrueCase %s", testName) 69 | if fail > 0: 70 | return False 71 | return True 72 | -------------------------------------------------------------------------------- /requirements: -------------------------------------------------------------------------------- 1 | xlrd 2 | pymysql 3 | requests -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | # author: 赫本z 4 | # 验证包:接口测试脚本 5 | 6 | import core.log as log 7 | from function.func import ApiTest 8 | 9 | func = ApiTest() 10 | logging = log.get_logger() 11 | 12 | """1.外部输入参数""" 13 | 14 | module = 'user' 15 | url = 'http://127.0.0.1:8080' 16 | 17 | """2.根据module获取Sheet""" 18 | logging.info("-------------- Execute TestCases ---------------") 19 | sheet = func.get_excel_sheet(func.filename, module) 20 | 21 | # """3.数据准备""" 22 | # logging.info("-------------- Prepare data through MysqlDB --------------") 23 | # sql = func.get_prepare_sql(sheet) 24 | # func.prepare_data(host=host, user=user, password=password, db=db, sql=sql) 25 | 26 | """4.执行测试用例""" 27 | res = func.run_test(sheet, url) 28 | logging.info("-------------- Get the result ------------ %s", res) 29 | -------------------------------------------------------------------------------- /test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F1And/api4excel/d60cc7f1f764157fd7805fed66b9fc22d8bdaf7e/test.xlsx --------------------------------------------------------------------------------