接口自动化测试框架
一、框架思路
读取测试用例——>对数据进行结构化——>根据url、method、params发请求——>校验响应数据——>记录日志——>生成测试报告。
二、框架目录结构

三、预安装模块
pip install ddt
pip install requests
pip install openpyxl
pip install schedule
pip install xlsxwriter
三、框架代码
程序入口,run.py,使用unittest模块执行测试用例
""" 程序入口,执行测试用例,生成HTML和EXCEL测试报告,发送邮件 """ import os import time import unittest import schedule from Common import HTMLTestReportCN from Common.test_api import TestAPI from Common.send_email import SendEmail from Common.excel_report import ExcelReport from settings import html_report_path, excel_report_path, timing, timing_run, project_name def main(): # 实例化测试套件 suite = unittest.TestSuite() loader = unittest.TestLoader() # 加载测试用例 suite.addTest(loader.loadTestsFromTestCase(TestAPI)) # 生成报告路径 start_time = time.strftime('%Y%m%d_%H%M%S', time.localtime()) html_file_path = os.path.join(html_report_path, 'report_{}.html'.format(start_time)) excel_file_path = os.path.join(excel_report_path, 'report_{}.xlsx'.format(start_time)) # 执行测试用例并生成HTML测试报告 with open(html_file_path, 'wb') as f: runner = HTMLTestReportCN.HTMLTestRunner(stream=f, verbosity=2, title='{} API Test'.format(project_name)) runner.run(suite) # 生成EXCEL测试报告 from Common.test_api import CaseCount, TestData report = ExcelReport(CaseCount, TestData, excel_file_path) report.create_report() # 发送邮件 # SendEmail([html_file_path, excel_file_path]).send_mails() if __name__ == '__main__': if timing_run: # 设置定时任务 schedule.every().day.at(timing).do(main) while True: schedule.run_pending() time.sleep(10) else: main()
读取测试用例,使用openpyxl模块
import os from openpyxl import load_workbook from settings import test_data_path, test_file def file_path_list(): """ 根据配置文件读取testFile下的用例文件并生成路径 :return: """ if test_file: return (os.path.join(test_data_path, file) for file in test_file) else: files = os.listdir(test_data_path) return (os.path.join(test_data_path, file) for file in files) def test_case_data(): """ 从测试用例文件读取数据,并把数据封装成一个列表 :return: 测试数据列表 """ test_data_list = [] path_list = file_path_list() for path in path_list: wb = load_workbook(path) for sheetname in wb.sheetnames: if sheetname == '用例说明': continue sheet = wb[sheetname] for row in range(2, sheet.max_row + 1): dic = {} for col in range(1, sheet.max_column + 1): dic[sheet.cell(1, col).value] = sheet.cell(row, col).value test_data_list.append(dic) wb.close() return test_data_list
封装请求类,使用requests模块
""" 封装请求类,处理不同方法的请求 """ import os import base64 import requests from settings import proxies, username, password, upload_path class HttpRequest: def __init__(self, obj, url): self.obj = obj self.url = url def create_auth(self): """ 根据用户名和密码生成Authorization信息 :return: """ return base64.b64encode('{}:{}'.format(username, password).encode('utf-8')).decode('utf-8') def http_request(self, method, params): """ 处理发送json数据的请求 :param method: :param params: :return: """ headers = {'Authorization': 'Basic {}'.format(self.create_auth()), 'Content-Type': 'application/json'} method = method.lower() res = requests.request(method, self.url, headers=headers, proxies=proxies, data=params) return res def http_request_upload(self, method, params, upload_file): """ 处理上传文件的请求 :param method: :param upload_file: :param params: :return: """ headers = {'Authorization': 'Basic {}'.format(self.create_auth())} files = {} for k, v in upload_file.items(): files[k] = open(os.path.join(upload_path, v), 'rb') res = requests.request(method, self.url, headers=headers, proxies=proxies, data=eval(params), files=files) return res def send_request(self, method, params, upload_file): """ 根据upload_file判断调用哪个方法发请求 :param method: 请求方法 :param params: 请求参数 :param upload_file: 上传文件参数 :return: 响应结果 """ if upload_file: return self.http_request_upload(method, params, eval(upload_file)) else: return self.http_request(method, params)
执行测试用例,使用unittest的TestCase生成测试用例
""" 执行测试用例,发请求,对响应结果进行校验,记录日志 """ import copy import time import unittest from ddt import ddt, data from settings import base_url from Common.log import MyLog from Common.http_request import HttpRequest from Common.validate_data import ValidateData from Common.extract_case import test_case_data # 获取测试数据 TestData = [] # 统计测试成功、失败的数量 CaseCount = {} @ddt class TestAPI(unittest.TestCase): @classmethod def setUpClass(cls): cls.logger = MyLog() cls.field_dic = {} cls.logger.info('本次测试开始,测试时间:{}'.format(time.strftime('%Y-%m%d %H:%M:%S'), time.localtime())) global TestData global CaseCount TestData.clear() CaseCount = {'test_sum': 0, 'test_success': 0, 'test_failed': 0} def setUp(self): self.t = time.time() CaseCount['test_sum'] += 1 @data(*test_case_data()) def test_api(self, item): obj = ValidateData(self) dic = copy.deepcopy(item) try: # 处理用例中的变量 dic = obj.deal_variable(dic) # 校验用例的合法性 obj.check_case_legitimacy(dic) url = base_url + dic['URL'] self.logger.info('用例ID:{}'.format(dic['用例ID'])) self.logger.info('用例标题:{}'.format(dic['用例名称'])) self.logger.info('URL:{}'.format(url)) # 发送请求 http = HttpRequest(self, url) res = http.send_request(dic['请求方法'], dic['参数'], dic['文件路径']) dic['URL'] = url # 校验响应状态码 obj.check_code(dic['状态码'], res.status_code) self.logger.info('期望值:{}'.format(dic['期望值'])) self.logger.info('实际值:{}'.format(res.text)) dic['实际值'] = res.text # 校验期望值 obj.check_expectations(dic['期望值'], res) # 保存字段 obj.save_field(dic['保存的字段'], res.json()) self.logger.info('用例执行通过。') dic['测试结果'] = '成功' CaseCount['test_success'] += 1 except Exception as e: self.logger.error('用例执行失败。') dic['测试结果'] = '失败' CaseCount['test_failed'] += 1 raise e finally: self.case_id = dic['用例ID'] self.title = dic['用例名称'] TestData.append(dic) def tearDown(self): self.logger.info('用例执行时间:{}秒'.format(time.time() - self.t)) @classmethod def tearDownClass(cls): cls.logger.info('本次测试结束。\n')
校验数据类,在执行测试用例过程中,对响应数据进行校验
import re import json class ValidateData: def __init__(self, obj): self.obj = obj def deal_variable(self, item): """ 将用例中的参数替换成相应的值 :param item: 用例字典 :return: 替换参数后的用例字典 """ for k in item: if isinstance(item[k], str): while True: ret = re.search('\${.*?}', item[k]) try: if ret: item[k] = item[k].replace(ret.group(), str(self.obj.field_dic[ret.group()[2:-1]])) else: break except KeyError as e: self.obj.logger.error('替换字段{}失败,没有该字段。'.format(ret.group())) raise e return item def save_field(self, fields, response): """ 把响应中需要保存的字段保存到字典中 :param field: 需要保存的字段 :param response: 响应的内容 :return: """ if fields: fields = eval(fields) for field in fields: if not response.get(field): self.obj.logger.error('保存字段失败,响应中没有{}字段'.format(field)) raise Exception('保存字段失败,响应中没有{}字段'.format(field)) self.obj.field_dic[fields[field]] = response.get(field) def check_case_legitimacy(self, item): """ 校验用例的合法性 :param item: 用例字典 :return: """ case_head = ['用例ID', '用例名称', '请求方法', 'URL', '参数', '文件路径', '状态码', '期望值', '实际值', '保存的字段', '测试结果'] method_list = ['get', 'post', 'put', 'patch', 'delete', 'options'] if list(item.keys()) != case_head: self.obj.logger.error('用例模板错误,请不要擅自修改模板!') raise Exception('用例模板错误,请不要擅自修改模板!') for k in item: if k == '请求方法' and item[k].lower() not in method_list: self.obj.logger.error('用例请求方式错误!只支持发{}请求。'.format('、'.join(method_list))) raise Exception('用例请求方式错误!只支持发{}请求。'.format('、'.join(method_list))) elif k in ['参数', '期望值']: if item[k]: try: json.loads(item[k]) except Exception as e: self.obj.logger.error('用例格式错误,{}字段请填写json格式的数据!'.format(k)) raise e def check_code(self, hope_code, res_code): """ 校验状态码是否与预期一致 :param hope_code: 预期的响应状态码 :param res_code: 实际的响应状态码 :return: """ try: self.obj.assertEqual(hope_code, res_code) self.obj.logger.info('响应状态码为{},与预期一致。'.format(hope_code)) except AssertionError as e: self.obj.logger.error('响应状态码错误,预期是{},实际是{}。'.format(hope_code, res_code)) self.obj.logger.error('错误信息:{}'.format(str(e))) # raise e def check_field_type(self, key, hope_type, res_value): """ 校验字段的类型 :param key: 需要校验的字段 :param hope_type: 需要校验字段的类型 :param res_value: 响应的内容 :return: """ try: self.obj.assertEqual(eval(hope_type), type(res_value)) self.obj.logger.info('字段{}的类型为{},与预期一致。'.format(key, hope_type)) except AssertionError as e: self.obj.logger.error('字段{}的类型为{},与预期不一致。'.format(key, type(res_value))) raise e def check_str(self, hope, res): """ 校验字符串格式的期望值 :param hope: 期望值 :param res: 响应的内容 :return: """ try: self.obj.assertEqual(hope, res) except AssertionError as e: self.obj.logger.error('返回值校验错误,预期值为{},实际值为{}'.format(hope, str)) raise e def check_dict(self, hope, res): """ 校验字典格式的期望值 :param hope: 期望值 :param res: 响应的内容 :return: """ for k in hope: self.obj.logger.info('类型:{}'.format(type(res))) try: self.obj.assertEqual(hope[k], res[k]) self.obj.logger.info('字段{}校验正确'.format(k)) except KeyError as e1: self.obj.logger.error('字段错误,没有{}字段'.format(k)) raise e1 except AssertionError as e2: if hope[k] in ['int', 'float', 'str', 'dict', 'list', 'tuple']: self.check_field_type(k, hope[k], res[k]) else: self.obj.logger.error('字段{}校验错误,预期值为{},实际值为{}'.format(k, hope[k], res[k])) raise e2 def check_expectations(self, hope, response): """ 校验期望值 :param hope: 期望值 :param response: 响应的内容 :return: """ if hope: hope = json.loads(hope) try: res = response.json() except Exception: self.obj.logger.error('返回值格式错误,不是json格式!请检查URL和请求方法是否正确。') raise Exception('返回值格式错误,不是json格式!') if isinstance(hope, str): self.check_str(hope, str) elif isinstance(hope, dict): self.check_dict(hope, res) elif isinstance(hope, list) or isinstance(hope, tuple): for i in range(len(hope)): if isinstance(hope[i], str): self.check_str(hope[i], res[i]) elif isinstance(hope[i], dict): self.check_dict(hope[i], res[i]) else: self.obj.logger.info('不支持校验的数据类型。') raise Exception('不支持校验的数据类型。')
封装日志类,用来生成日志
import os import time import logging from settings import log_path class MyLog: def my_log(self, msg, msg_level, log_name='interfaceTest', level='INFO'): logger = logging.getLogger(log_name) # 日志收集器的级别 logger.setLevel(level) # 输出渠道 相对路径 file_path = os.path.join(log_path, 'log_{}.txt'.format(time.strftime('%Y%m%d', time.localtime()))) fh = logging.FileHandler(file_path, encoding='UTF-8') sh = logging.StreamHandler() # 输出渠道的级别 fh.setLevel(level) sh.setLevel(level) formatter = logging.Formatter('[%(levelname)s]%(asctime)s[日志信息]:%(message)s') fh.setFormatter(formatter) sh.setFormatter(formatter) # 对接日志收集器 以及输出渠道 logger.addHandler(sh) logger.addHandler(fh) if msg_level == 'DEBUG': logger.debug(msg) elif msg_level == 'INFO': logger.info(msg) elif msg_level == 'WARNING': logger.warning(msg) elif msg_level == 'ERROR': logger.error(msg) elif msg_level == 'CRITICAL': logger.critical(msg) # 移除handler logger.removeHandler(fh) logger.removeHandler(sh) def debug(self, msg): self.my_log(msg, 'DEBUG') def info(self, msg): self.my_log(msg, 'INFO') def warning(self, msg): self.my_log(msg, 'WARNING') def error(self, msg): self.my_log(msg, 'ERROR') def critical(self, msg): self.my_log(msg, 'CRITICAL')
封装生成Excel报告类
""" 封装生成EXCEL测试报告类,通过xlsxwriter模块生成EXCEL测试报告 """ import time import xlsxwriter from settings import project_name, version, tester, graph class ExcelReport: def __init__(self, data, content, excel_file_path): """ :param data: :param content: :param excel_file_path: """ self.data = data self.content = content self.workbook = xlsxwriter.Workbook(excel_file_path) def get_format(self, option=None): return self.workbook.add_format(option) # 设置居中 def get_format_center(self, num=1): return self.workbook.add_format({'align': 'center', 'valign': 'vcenter', 'border': num}) def set_border_(self, num=1): return self.workbook.add_format({}).set_border(num) # 写数据 def _write_center(self, worksheet, cl, data): return worksheet.write(cl, data, self.get_format_center()) # 生成饼形图 def pie(self, worksheet): dic = {'柱状图': 'column', '饼图': 'pie', '圆环': 'doughnut', '条形图': 'bar'} chart1 = self.workbook.add_chart({'type': dic[graph]}) chart1.add_series({ 'name': '接口测试统计', 'categories': '=测试总况!$D$4:$D$5', 'values': '=测试总况!$E$4:$E$5', }) chart1.set_title({'name': '接口测试统计'}) chart1.set_style(10) worksheet.insert_chart('A9', chart1, {'x_offset': 200, 'y_offset': 10}) def init(self, worksheet): # 设置列行的宽高 col_list = ['A', 'B', 'C', 'D', 'E', 'F'] for col in col_list: worksheet.set_column('{}:{}'.format(col, col), 20) for row in range(1, 6): worksheet.set_row(row, 30) define_format_H1 = self.get_format({'bold': True, 'font_size': 18}) define_format_H2 = self.get_format({'bold': True, 'font_size': 14}) define_format_H1.set_border(1) define_format_H2.set_border(1) define_format_H1.set_align("center") define_format_H2.set_align("center") define_format_H2.set_bg_color("blue") define_format_H2.set_color("#ffffff") # Create a new Chart object. worksheet.merge_range('A1:F1', '接口测试报告总概况', define_format_H1) worksheet.merge_range('A2:F2', '测试概括', define_format_H2) worksheet.merge_range('A3:A6', '这里放图片', self.get_format_center()) self._write_center(worksheet, "B3", '项目名称') self._write_center(worksheet, "B4", '接口版本') self._write_center(worksheet, "B5", '脚本语言') self._write_center(worksheet, "B6", '测试人') self._write_center(worksheet, "C3", project_name) self._write_center(worksheet, "C4", version) self._write_center(worksheet, "C5", 'Python') self._write_center(worksheet, "C6", tester) self._write_center(worksheet, "D3", "接口总数") self._write_center(worksheet, "D4", "通过总数") self._write_center(worksheet, "D5", "失败总数") self._write_center(worksheet, "D6", "测试日期") self._write_center(worksheet, "E3", self.data['test_sum']) self._write_center(worksheet, "E4", self.data['test_success']) self._write_center(worksheet, "E5", self.data['test_failed']) self._write_center(worksheet, "E6", time.strftime('%Y-%m-%d %H:%M', time.localtime())) self._write_center(worksheet, "F3", "通过率") worksheet.merge_range('F4:F6', '{}%'.format(int((self.data['test_success'] / self.data['test_sum']) * 100)), self.get_format_center()) self.pie(worksheet) def test_detail(self, worksheet): # 设置列行的宽高 worksheet.set_column("A:A", 10) worksheet.set_column("B:B", 30) worksheet.set_column("C:C", 10) worksheet.set_column("D:D", 40) worksheet.set_column("E:E", 30) worksheet.set_column("F:F", 40) worksheet.set_column("G:G", 40) worksheet.set_column("H:H", 10) for i in range(1, len(self.content) + 3): worksheet.set_row(i, 30) worksheet.merge_range('A1:H1', '测试详情', self.get_format({'bold': True, 'font_size': 18, 'align': 'center', 'valign': 'vcenter', 'bg_color': 'blue', 'font_color': '#ffffff'})) self._write_center(worksheet, "A2", '用例ID') self._write_center(worksheet, "B2", '用例名称') self._write_center(worksheet, "C2", '请求方式') self._write_center(worksheet, "D2", 'URL') self._write_center(worksheet, "E2", '参数') self._write_center(worksheet, "F2", '期望值') self._write_center(worksheet, "G2", '实际值') self._write_center(worksheet, "H2", '测试结果') temp = 3 for item in self.content: self._write_center(worksheet, "A" + str(temp), item.get('用例ID')) self._write_center(worksheet, "B" + str(temp), item.get('用例名称')) self._write_center(worksheet, "C" + str(temp), item.get('请求方法')) self._write_center(worksheet, "D" + str(temp), item.get('URL')) self._write_center(worksheet, "E" + str(temp), item.get('参数')) self._write_center(worksheet, "F" + str(temp), item.get('期望值')) self._write_center(worksheet, "G" + str(temp), item.get('实际值')) self._write_center(worksheet, "H" + str(temp), item.get('测试结果')) temp += 1 def create_report(self): worksheet = self.workbook.add_worksheet("测试总况") worksheet2 = self.workbook.add_worksheet("测试详情") self.init(worksheet) self.test_detail(worksheet2) self.workbook.close()
封装邮件类,用来发送邮件
import os import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication from settings import email_from, email_pwd, email_to, smtp_service, email_title, email_content class SendEmail: def __init__(self, path_list): """ 初始化方法 :param path_list: 附件的路径 """ self.path_list = path_list def send_mails(self): """ 发送邮件方法 :return: """ msg = MIMEMultipart() msg['From'] = email_from msg['To'] = email_to msg['Subject'] = email_title content = MIMEText(email_content) msg.attach(content) for path in self.path_list: with open(path, 'rb') as f: ret = f.read() accessory = MIMEApplication(ret) accessory.add_header('Content-Disposition', 'attachment', filename=os.path.split(path)[1]) msg.attach(accessory) s = smtplib.SMTP_SSL(smtp_service, timeout=30) s.login(email_from, email_pwd) s.sendmail(email_from, email_to, msg.as_string()) s.close()

浙公网安备 33010602011771号