背景介绍
趁着休息的时间,把以前的接口自动化框架再优化一下,等全部功能模块改完后再把东西放到github上,现在开个记录帖。
正文
有时候也会问自己为什么要重复造轮子,开源框架一搜一堆。后来想想,可能我在乎的不是目的地,而是沿途的风景。
【流程图】
总体的框架流程图如下所示:
【Common】
常见的接口都是走http协议,对requests库进行post/get请求方法的封装。
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:Request.py @E-mail:364942727@qq.com @Time:2020/9/5 8:29 下午 @Author:Nobita @Version:1.0 @Desciption:Request请求封装模块 """ import requests from Common.Log import logger class RunMain(): def __init__(self): self.logger = logger def send_post(self, url, headers, data): # 定义一个方法,传入需要的参数url、headers和data # 参数必须按照url、headers、data顺序传入 headers = headers result_data = requests.post(url=url, headers=headers, data=data).json() # 因为这里要封装post方法,所以这里的url和data值不能写死 result_json = requests.post(url=url, headers=headers, json=data).json() # 接口需要json参数提交数据,用这种请求方法 # res = json.dumps(Log, ensure_ascii=False, sort_keys=True, indent=2) # 格式化输出 res = result_data return res def send_get(self, url, headers, data): headers = headers result_data = requests.get(url=url, headers=headers, data=data).json() result_json = requests.post(url=url, headers=headers, json=data).json() # 接口需要json参数提交数据,用这种请求方法 # res = json.dumps(Log, ensure_ascii=False, sort_keys=True, indent=2) # 格式化输出 res = result_data return res def run_main(self, method, url=None, headers=None, data=None): # 定义一个run_main函数,通过传过来的method来进行不同的get或post请求 result = None if method == 'post': result = self.send_post(url, headers, data) self.logger.info(str(result)) elif method == 'get': result = self.send_get(url, headers, data) self.logger.info(str(result)) else: print("method值错误!!!") self.logger.info("method值错误!!!") return result if __name__ == '__main__': # 通过写死参数,来验证我们写的请求是否正确 pass # method_post = 'post' # url_post = 'http://127.0.0.1:5000/login' # data_post = { # "username": "admin", # "password": "a123456" # } # result_post = RunMain().run_main(method=method_post, url=url_post, data=data_post) # print(result_post)
对发送邮件的SMTP模块进行封装。
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:SendEmail.py @E-mail:364942727@qq.com @Time:2020/9/5 7:58 下午 @Author:Nobita @Version:1.0 @Desciption:封装SMTP邮件功能模块 """ import os from Config import readConfig import getpathInfo import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header from Common.Log import logger class SendEmail(object): def __init__(self): # 读取邮件配置信息,初始化参数 read_conf = readConfig.ReadConfig() self.email_service = read_conf.get_email('EMAIL_SERVICE') # 从配置文件中读取,邮件服务器类型 self.email_port = read_conf.get_email('EMAIL_PORT') # 从配置文件中读取,邮件服务器端口 self.sender_address = read_conf.get_email('SENDER_ADDRESS') # 从配置文件中读取,发件人邮箱地址 self.sender_password = read_conf.get_email('SENDER_PASSWORD') # 从配置文件中读取,发件人邮箱授权码 self.receiver_address = read_conf.get_email('RECEIVER_ADDRESS') # 从配置文件中读取,收件人邮箱地址 self.file_path = os.path.join(getpathInfo.get_Path(), 'Report', 'report.html') # 获取测试报告路径 # 日志输出 self.logger = logger def send_email(self): # 第三方 SMTP 服务 message = MIMEMultipart() # 创建附件的实例 message['From'] = Header("测试组", 'utf-8') message['To'] = Header(''.join(self.receiver_address), 'utf-8') subject = '接口测试邮件' message['Subject'] = Header(subject, 'utf-8') # 邮件正文内容 part = MIMEText('Dear all: 附件为接口自动化测试报告,此为自动发送邮件,请勿回复,谢谢!', 'plain', 'utf-8') message.attach(part) # 发送附件 att1 = MIMEText(open(file=self.file_path, mode='r').read(), 'base64', 'utf-8') att1["Content-Type"] = 'application/octet-stream' att1.add_header('Content-Disposition', 'attachment', filename=('utf-8', '', '接口测试报告.html')) message.attach(att1) try: service = smtplib.SMTP_SSL(self.email_service) # service.set_debuglevel(True) # debug开启或关闭 service.connect(self.email_service, self.email_port) service.login(self.sender_address, self.sender_password) service.sendmail(self.sender_address, self.receiver_address, message.as_string()) print('邮件发送成功') service.close() self.logger.info("{'邮件发送成功'}") except smtplib.SMTPException: print("报错,邮件发送失败") self.logger.info("{'报错,邮件发送失败'}") if __name__ == '__main__': # SendEmail().send_email() # 测试邮件功能模块 pass
常见assert断言模块的封装。
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:Assert.py @E-mail:364942727@qq.com @Time:2020/9/5 23:03 下午 @Author:Nobita @Version:1.0 @Desciption:Assert断言封装模块 """ from Common.Log import logger import json class Assertions: def __init__(self): self.log = logger def assert_code(self, code, expected_code): """ 验证response状态码 :param code: :param expected_code: :return: """ try: assert code == expected_code return True except: self.log.info("statusCode error, expected_code is %s, statusCode is %s " % (expected_code, code)) raise def assert_body(self, body, body_msg, expected_msg): """ 验证response body中任意属性的值 :param body: :param body_msg: :param expected_msg: :return: """ try: msg = body[body_msg] assert msg == expected_msg return True except: self.log.info( "Response body msg != expected_msg, expected_msg is %s, body_msg is %s" % (expected_msg, body_msg)) raise def assert_in_text(self, body, expected_msg): """ 验证response body中是否包含预期字符串 :param body: :param expected_msg: :return: """ try: text = json.dumps(body, ensure_ascii=False) # print(text) assert expected_msg in text return True except: self.log.info("Response body Does not contain expected_msg, expected_msg is %s" % expected_msg) raise def assert_text(self, body, expected_msg): """ 验证response body中是否等于预期字符串 :param body: :param expected_msg: :return: """ try: assert body == expected_msg return True except: self.log.info("Response body != expected_msg, expected_msg is %s, body is %s" % (expected_msg, body)) raise def assert_time(self, time, expected_time): """ 验证response body响应时间小于预期最大响应时间,单位:毫秒 :param body: :param expected_time: :return: """ try: assert time < expected_time return True except: self.log.info("Response time > expected_time, expected_time is %s, time is %s" % (expected_time, time)) raise if __name__ == '__main__': # info_body = {'code': 102001, 'message': 'login success'} # Assert = Assertions() # expect_code = 10200 # Assert.assert_code(info_body['code'], expect_code) pass
对Log日志模块的封装。
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:Log.py @E-mail:364942727@qq.com @Time:2020/9/4 8:58 下午 @Author:Nobita @Version:1.0 @Desciption:Log日志模块 """ import os import logging from logging.handlers import TimedRotatingFileHandler import getpathInfo class Logger(object): def __init__(self, logger_name='logs…'): global log_path path = getpathInfo.get_Path() log_path = os.path.join(path, 'Log') # 存放log文件的路径 self.logger = logging.getLogger(logger_name) logging.root.setLevel(logging.NOTSET) self.log_file_name = 'logs' # 日志文件的名称 self.backup_count = 5 # 最多存放日志的数量 # 日志输出级别 self.console_output_level = 'WARNING' self.file_output_level = 'DEBUG' # 日志输出格式 self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') def get_logger(self): """在logger中添加日志句柄并返回,如果logger已有句柄,则直接返回""" if not self.logger.handlers: # 避免重复日志 console_handler = logging.StreamHandler() console_handler.setFormatter(self.formatter) console_handler.setLevel(self.console_output_level) self.logger.addHandler(console_handler) # 每天重新创建一个日志文件,最多保留backup_count份 file_handler = TimedRotatingFileHandler(filename=os.path.join(log_path, self.log_file_name), when='D', interval=1, backupCount=self.backup_count, delay=True, encoding='utf-8') file_handler.setFormatter(self.formatter) file_handler.setLevel(self.file_output_level) self.logger.addHandler(file_handler) return self.logger logger = Logger().get_logger() if __name__ == "__main__": pass
对各种常见加密方法的封装。
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:Hash.py @E-mail:364942727@qq.com @Time:2020/9/6 15:55 下午 @Author:Nobita @Version:1.0 @Desciption:封装各种常用的加密方法 """ from hashlib import sha1 from hashlib import md5 from Crypto.Hash import SHA256 from Crypto.Cipher import AES from Crypto.Cipher import DES import binascii class MyHash(object): def my_md5(self, msg): """ md5 算法加密 :param msg: 需加密的字符串 :return: 加密后的字符 """ hl = md5() hl.update(msg.encode('utf-8')) return hl.hexdigest() def my_sha1(self, msg): """ sha1 算法加密 :param msg: 需加密的字符串 :return: 加密后的字符 """ sh = sha1() sh.update(msg.encode('utf-8')) return sh.hexdigest() def my_sha256(self, msg): """ sha256 算法加密 :param msg: 需加密的字符串 :return: 加密后的字符 """ sh = SHA256.new() sh.update(msg.encode('utf-8')) return sh.hexdigest() def my_des(self, msg, key): """ DES 算法加密 :param msg: 需加密的字符串,长度必须为8的倍数,不足添加'=' :param key: 8个字符 :return: 加密后的字符 """ de = DES.new(key, DES.MODE_ECB) mss = msg + (8 - (len(msg) % 8)) * '=' text = de.encrypt(mss.encode()) return binascii.b2a_hex(text).decode() def my_aes_encrypt(self, msg, key, vi): """ AES 算法的加密 :param msg: 需加密的字符串 :param key: 必须为16,24,32位 :param vi: 必须为16位 :return: 加密后的字符 """ obj = AES.new(key, AES.MODE_CBC, vi) txt = obj.encrypt(msg.encode()) return binascii.b2a_hex(txt).decode() def my_aes_decrypt(self, msg, key, vi): """ AES 算法的解密 :param msg: 需解密的字符串 :param key: 必须为16,24,32位 :param vi: 必须为16位 :return: 加密后的字符 """ msg = binascii.a2b_hex(msg) obj = AES.new(key, AES.MODE_CBC, vi) return obj.decrypt(msg).decode() if __name__ == "__main__": res = MyHash().my_md5('hello world') print(res)
获取配置文件中拼接后的base_url
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:geturlParams.py @E-mail:364942727@qq.com @Time:2020/9/3 9:28 下午 @Author:Nobita @Version:1.0 @Desciption:获取配置文件中拼接后的URL """ from Config import readConfig as readConfig class geturlParams(): # 定义一个方法,将从配置文件中读取的进行拼接 def __init__(self): self.readconfig = readConfig.ReadConfig() def get_Url(self): new_url = self.readconfig.get_http('scheme') + '://' + self.readconfig.get_http( 'baseurl') + ':' + self.readconfig.get_http( 'port') # logger.info('new_url'+new_url) return new_url if __name__ == '__main__': # 验证拼接后的正确性 print(geturlParams().get_Url()) # pass
对读取Excel文件方法的封装。
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:readExcel.py @E-mail:364942727@qq.com @Time:2020/9/3 16:58 上午 @Author:Nobita @Version:1.0 @Desciption: """ import os import getpathInfo from xlrd import open_workbook # 调用读Excel的第三方库xlrd class readExcel(): def __init__(self): self.path = getpathInfo.get_Path() # 拿到该项目所在的绝对路径 def get_xls(self, xls_name, sheet_name): # xls_name填写用例的Excel名称 sheet_name该Excel的sheet名称 cls = [] # 获取用例文件路径 xlsPath = os.path.join(self.path, "TestFile", 'case', xls_name) file = open_workbook(xlsPath) # 打开用例Excel sheet = file.sheet_by_name(sheet_name) # 获得打开Excel的sheet # 获取这个sheet内容行数 nrows = sheet.nrows for i in range(nrows): # 根据行数做循环 if sheet.row_values(i)[0] != u'case_name': # 如果这个Excel的这个sheet的第i行的第一列不等于case_name那么我们把这行的数据添加到cls[] cls.append(sheet.row_values(i)) return cls if __name__ == '__main__': # 我们执行该文件测试一下是否可以正确获取Excel中的值 print(readExcel().get_xls('learning-API-test_Case.xlsx', 'login')) # 遍历每一行数据 print(readExcel().get_xls('learning-API-test_Case.xlsx', 'login')[0][1]) # 登录接口url print(readExcel().get_xls('learning-API-test_Case.xlsx', 'login')[1][4]) # 请求method # pass
对生成html接口自动化报告方法的封装。
#coding=utf-8 """ A TestRunner for use with the Python unit testing framework. It generates a HTML report to show the Log at a glance. The simplest way to use this is to invoke its main method. E.g. import unittest import HTMLTestReportCN ... define your tests ... if __name__ == '__main__': HTMLTestReportCN.main() For more customization options, instantiates a HTMLTestReportCN object. HTMLTestReportCN is a counterpart to unittest's TextTestRunner. E.g. # output to a file fp = file('my_report.html', 'wb') runner = HTMLTestReportCN.HTMLTestReportCN( stream=fp, title='My unit test', description='This demonstrates the report output by HTMLTestReportCN.' ) # Use an external stylesheet. # See the Template_mixin class for more customizable options runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">' # run the test runner.run(my_test_suite) ------------------------------------------------------------------------ Copyright (c) 2004-2007, Wai Yip Tung Copyright (c) 2017, Findyou All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Wai Yip Tung nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ # URL: http://tungwaiyip.info/software/HTMLTestRunner.html __author__ = "Wai Yip Tung, Findyou" __version__ = "0.8.3" """ Change History Version 0.8.3 -Findyou 20171206 * BUG fixed :错误的测试用例没有统计与显示 * BUG fixed :当PASS的测试用例有print内容时,通过按钮显示为红色 * 表格背景颜色根据用例结果显示颜色,优先级: 错误(黄色)>失败(红色)>通过(绿色) * 合并文为HTMLTestRunner*N.py 同时支持python2,python3 Version 0.8.2.2 -Findyou * HTMLTestRunnerEN.py 支持 python3.x * HTMLTestRunnerEN.py 支持 python2.x Version 0.8.2.1 -Findyou * 支持中文,汉化 * 调整样式,美化(需要连入网络,使用的百度的Bootstrap.js) * 增加 通过分类显示、测试人员、通过率的展示 * 优化“详细”与“收起”状态的变换 * 增加返回顶部的锚点 Version 0.8.2 * Show output inline instead of popup window (Viorel Lupu). Version in 0.8.1 * Validated XHTML (Wolfgang Borgert). * Added description of test classes and test cases. Version in 0.8.0 * Define Template_mixin class for customization. * Workaround a IE 6 bug that it does not treat <script> block as CDATA. Version in 0.7.1 * Back port to Python 2.3 (Frank Horowitz). * Fix missing scroll bars in detail log (Podi). """ # TODO: color stderr # TODO: simplify javascript using ,ore than 1 class in the class attribute? import datetime try: from StringIO import StringIO except ImportError: from io import StringIO import sys import time import unittest from xml.sax import saxutils try: reload(sys) sys.setdefaultencoding('utf-8') except NameError: pass # ------------------------------------------------------------------------ # The redirectors below are used to capture output during testing. Output # sent to sys.stdout and sys.stderr are automatically captured. However # in some cases sys.stdout is already cached before HTMLTestRunner is # invoked (e.g. calling logging.basicConfig). In order to capture those # output, use the redirectors for the cached stream. # # e.g. # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector) # >>> class OutputRedirector(object): """ Wrapper to redirect stdout or stderr """ def __init__(self, fp): self.fp = fp def write(self, s): self.fp.write(s) def writelines(self, lines): self.fp.writelines(lines) def flush(self): self.fp.flush() stdout_redirector = OutputRedirector(sys.stdout) stderr_redirector = OutputRedirector(sys.stderr) # ---------------------------------------------------------------------- # Template class Template_mixin(object): """ Define a HTML template for report customerization and generation. Overall structure of an HTML report HTML +------------------------+ |<html> | | <head> | | | | STYLESHEET | | +----------------+ | | | | | | +----------------+ | | | | </head> | | | | <body> | | | | HEADING | | +----------------+ | | | | | | +----------------+ | | | | REPORT | | +----------------+ | | | | | | +----------------+ | | | | ENDING | | +----------------+ | | | | | | +----------------+ | | | | </body> | |</html> | +------------------------+ """ STATUS = { 0: '通过', 1: '失败', 2: '错误', } DEFAULT_TITLE = '测试报告' DEFAULT_DESCRIPTION = '' DEFAULT_TESTER='QA' # ------------------------------------------------------------------------ # HTML Template HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>%(title)s</title> <meta name="generator" content="%(generator)s"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script> %(stylesheet)s </head> <body > %(heading)s %(report)s %(ending)s <script language="javascript" type="text/javascript"> output_list = Array(); // 修改按钮颜色显示错误问题 --Findyou v0.8.2.3 $("button[id^='btn_pt']").addClass("btn btn-success"); $("button[id^='btn_ft']").addClass("btn btn-danger"); $("button[id^='btn_et']").addClass("btn btn-warning"); /*level 增加分类并调整,增加error按钮事件 --Findyou v0.8.2.3 0:Pass //pt none, ft hiddenRow, et hiddenRow 1:Failed //pt hiddenRow, ft none, et hiddenRow 2:Error //pt hiddenRow, ft hiddenRow, et none 3:All //pt none, ft none, et none 4:Summary //all hiddenRow */ //add Error button event --Findyou v0.8.2.3 function showCase(level) { trs = document.getElementsByTagName("tr"); for (var i = 0; i < trs.length; i++) { tr = trs[i]; id = tr.id; if (id.substr(0,2) == 'ft') { if (level == 0 || level == 2 || level == 4 ) { tr.className = 'hiddenRow'; } else { tr.className = ''; } } if (id.substr(0,2) == 'pt') { if (level == 1 || level == 2 || level == 4) { tr.className = 'hiddenRow'; } else { tr.className = ''; } } if (id.substr(0,2) == 'et') { if (level == 0 || level == 1 || level == 4) { tr.className = 'hiddenRow'; } else { tr.className = ''; } } } //加入【详细】切换文字变化 --Findyou detail_class=document.getElementsByClassName('detail'); //console.log(detail_class.length) if (level == 3) { for (var i = 0; i < detail_class.length; i++){ detail_class[i].innerHTML="收起" } } else{ for (var i = 0; i < detail_class.length; i++){ detail_class[i].innerHTML="详细" } } } //add Error button event --Findyou v0.8.2.3 function showClassDetail(cid, count) { var id_list = Array(count); var toHide = 1; for (var i = 0; i < count; i++) { tid0 = 't' + cid.substr(1) + '_' + (i+1); tid = 'f' + tid0; tr = document.getElementById(tid); if (!tr) { tid = 'p' + tid0; tr = document.getElementById(tid); } if (!tr) { tid = 'e' + tid0; tr = document.getElementById(tid); } id_list[i] = tid; if (tr.className) { toHide = 0; } } for (var i = 0; i < count; i++) { tid = id_list[i]; //修改点击无法收起的BUG,加入【详细】切换文字变化 --Findyou if (toHide) { document.getElementById(tid).className = 'hiddenRow'; document.getElementById(cid).innerText = "详细" } else { document.getElementById(tid).className = ''; document.getElementById(cid).innerText = "收起" } } } function html_escape(s) { s = s.replace(/&/g,'&'); s = s.replace(/</g,'<'); s = s.replace(/>/g,'>'); return s; } </script> </body> </html> """ # variables: (title, generator, stylesheet, heading, report, ending) # ------------------------------------------------------------------------ # Stylesheet # # alternatively use a <link> for external style sheet, e.g. # <link rel="stylesheet" href="$url" type="text/css"> STYLESHEET_TMPL = """ <style type="text/css" media="screen"> body { font-family: Microsoft YaHei,Tahoma,arial,helvetica,sans-serif;padding: 20px; font-size: 100%; } table { font-size: 100%; } /* -- heading ---------------------------------------------------------------------- */ .heading { margin-top: 0ex; margin-bottom: 1ex; } .heading .description { margin-top: 4ex; margin-bottom: 6ex; } /* -- report ------------------------------------------------------------------------ */ #total_row { font-weight: bold; } .passCase { color: #5cb85c; } .failCase { color: #d9534f; font-weight: bold; } .errorCase { color: #f0ad4e; font-weight: bold; } .hiddenRow { display: none; } .testcase { margin-left: 2em; } </style> """ # ------------------------------------------------------------------------ # Heading # HEADING_TMPL = """<div class='heading'> <h1 style="font-family: Microsoft YaHei">%(title)s</h1> %(parameters)s <p class='description'>%(description)s</p> </div> """ # variables: (title, parameters, description) HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s : </strong> %(value)s</p> """ # variables: (name, value) # ------------------------------------------------------------------------ # Report # # 汉化,加美化效果 --Findyou REPORT_TMPL = """ <p id='show_detail_line'> <a class="btn btn-primary" href='javascript:showCase(4)'>概要{ %(passrate)s }</a> <a class="btn btn-success" href='javascript:showCase(0)'>通过{ %(Pass)s }</a> <a class="btn btn-danger" href='javascript:showCase(1)'>失败{ %(fail)s }</a> <a class="btn btn-warning" href='javascript:showCase(2)'>错误{ %(error)s }</a> <a class="btn btn-info" href='javascript:showCase(3)'>所有{ %(count)s }</a> </p> <table id='result_table' class="table table-condensed table-bordered table-hover"> <colgroup> <col align='left' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> <col align='right' /> </colgroup> <tr id='header_row' class="text-center active" style="font-weight: bold;font-size: 14px;"> <td>用例集/测试用例</td> <td>总计</td> <td>通过</td> <td>失败</td> <td>错误</td> <td>详细</td> </tr> %(test_list)s <tr id='total_row' class="text-center info"> <td>总计</td> <td>%(count)s</td> <td>%(Pass)s</td> <td>%(fail)s</td> <td>%(error)s</td> <td>通过率:%(passrate)s</td> </tr> </table> """ # variables: (test_list, count, Pass, fail, error ,passrate) REPORT_CLASS_TMPL = r""" <tr class='%(style)s'> <td>%(desc)s</td> <td class="text-center">%(count)s</td> <td class="text-center">%(Pass)s</td> <td class="text-center">%(fail)s</td> <td class="text-center">%(error)s</td> <td class="text-center"><a href="javascript:showClassDetail('%(cid)s',%(count)s)" class="detail" id='%(cid)s'>详细</a></td> </tr> """ # variables: (style, desc, count, Pass, fail, error, cid) #有output内容的样式,去掉原来JS效果,美化展示效果 -Findyou v0.8.2.3 REPORT_TEST_WITH_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'> <!--默认收起output信息 -Findyou <button id='btn_%(tid)s' type="button" class="btn-xs collapsed" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button> <div id='div_%(tid)s' class="collapse"> --> <!-- 默认展开output信息 -Findyou --> <button id='btn_%(tid)s' type="button" class="btn-xs" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button> <div id='div_%(tid)s' class="collapse in"> <pre> %(script)s </pre> </div> </td> </tr> """ # variables: (tid, Class, style, desc, status) # 无output内容样式改为button,按钮效果为不可点击 -Findyou v0.8.2.3 REPORT_TEST_NO_OUTPUT_TMPL = r""" <tr id='%(tid)s' class='%(Class)s'> <td class='%(style)s'><div class='testcase'>%(desc)s</div></td> <td colspan='5' align='center'><button id='btn_%(tid)s' type="button" class="btn-xs" disabled="disabled" data-toggle="collapse" data-target='#div_%(tid)s'>%(status)s</button></td> </tr> """ # variables: (tid, Class, style, desc, status) REPORT_TEST_OUTPUT_TMPL = r""" %(id)s: %(output)s """ # variables: (id, output) # ------------------------------------------------------------------------ # ENDING # # 增加返回顶部按钮 --Findyou ENDING_TMPL = """<div id='ending'> </div> <div style=" position:fixed;right:50px; bottom:30px; 20px; height:20px;cursor:pointer"> <a href="#"><span class="glyphicon glyphicon-eject" style = "font-size:30px;" aria-hidden="true"> </span></a></div> """ # -------------------- The end of the Template class ------------------- TestResult = unittest.TestResult class _TestResult(TestResult): # note: _TestResult is a pure representation of results. # It lacks the output and reporting ability compares to unittest._TextTestResult. def __init__(self, verbosity=1): TestResult.__init__(self) self.stdout0 = None self.stderr0 = None self.success_count = 0 self.failure_count = 0 self.error_count = 0 self.verbosity = verbosity # Log is a list of Log in 4 tuple # ( # Log code (0: success; 1: fail; 2: error), # TestCase object, # Test output (byte string), # stack trace, # ) self.result = [] #增加一个测试通过率 --Findyou self.passrate=float(0) def startTest(self, test): TestResult.startTest(self, test) # just one buffer for both stdout and stderr self.outputBuffer = StringIO() stdout_redirector.fp = self.outputBuffer stderr_redirector.fp = self.outputBuffer self.stdout0 = sys.stdout self.stderr0 = sys.stderr sys.stdout = stdout_redirector sys.stderr = stderr_redirector def complete_output(self): """ Disconnect output redirection and return buffer. Safe to call multiple times. """ if self.stdout0: sys.stdout = self.stdout0 sys.stderr = self.stderr0 self.stdout0 = None self.stderr0 = None return self.outputBuffer.getvalue() def stopTest(self, test): # Usually one of addSuccess, addError or addFailure would have been called. # But there are some path in unittest that would bypass this. # We must disconnect stdout in stopTest(), which is guaranteed to be called. self.complete_output() def addSuccess(self, test): self.success_count += 1 TestResult.addSuccess(self, test) output = self.complete_output() self.result.append((0, test, output, '')) if self.verbosity > 1: sys.stderr.write('ok ') sys.stderr.write(str(test)) sys.stderr.write(' ') else: sys.stderr.write('.') def addError(self, test, err): self.error_count += 1 TestResult.addError(self, test, err) _, _exc_str = self.errors[-1] output = self.complete_output() self.result.append((2, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(test)) sys.stderr.write(' ') else: sys.stderr.write('E') def addFailure(self, test, err): self.failure_count += 1 TestResult.addFailure(self, test, err) _, _exc_str = self.failures[-1] output = self.complete_output() self.result.append((1, test, output, _exc_str)) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(test)) sys.stderr.write(' ') else: sys.stderr.write('F') class HTMLTestReportCN(Template_mixin): """ """ def __init__(self, stream=sys.stdout, verbosity=1,title=None,description=None,tester=None): self.stream = stream self.verbosity = verbosity if title is None: self.title = self.DEFAULT_TITLE else: self.title = title if description is None: self.description = self.DEFAULT_DESCRIPTION else: self.description = description if tester is None: self.tester = self.DEFAULT_TESTER else: self.tester = tester self.startTime = datetime.datetime.now() def run(self, test): "Run the given test case or test suite." result = _TestResult(self.verbosity) test(result) self.stopTime = datetime.datetime.now() self.generateReport(test, result) # print >>sys.stderr, ' Time Elapsed: %s' % (self.stopTime-self.startTime) sys.stderr.write(' Time Elapsed: %s' % (self.stopTime-self.startTime)) return result def sortResult(self, result_list): # unittest does not seems to run in any particular order. # Here at least we want to group them together by class. rmap = {} classes = [] for n,t,o,e in result_list: cls = t.__class__ # if not rmap.has_key(cls): if cls not in rmap: rmap[cls] = [] classes.append(cls) rmap[cls].append((n,t,o,e)) r = [(cls, rmap[cls]) for cls in classes] return r #替换测试结果status为通过率 --Findyou def getReportAttributes(self, result): """ Return report attributes as a list of (name, value). Override this to add custom attributes. """ startTime = str(self.startTime)[:19] duration = str(self.stopTime - self.startTime) status = [] status.append('共 %s' % (result.success_count + result.failure_count + result.error_count)) if result.success_count: status.append('通过 %s' % result.success_count) if result.failure_count: status.append('失败 %s' % result.failure_count) if result.error_count: status.append('错误 %s' % result.error_count ) if status: status = ','.join(status) # 合入Github:boafantasy代码 if (result.success_count + result.failure_count + result.error_count) > 0: self.passrate = str("%.2f%%" % (float(result.success_count) / float(result.success_count + result.failure_count + result.error_count) * 100)) else: self.passrate = "0.00 %" else: status = 'none' return [ (u'测试人员', self.tester), (u'开始时间',startTime), (u'合计耗时',duration), (u'测试结果',status + ",通过率= "+self.passrate), ] def generateReport(self, test, result): report_attrs = self.getReportAttributes(result) generator = 'HTMLTestReportCN %s' % __version__ stylesheet = self._generate_stylesheet() heading = self._generate_heading(report_attrs) report = self._generate_report(result) ending = self._generate_ending() output = self.HTML_TMPL % dict( title = saxutils.escape(self.title), generator = generator, stylesheet = stylesheet, heading = heading, report = report, ending = ending, ) self.stream.write(output.encode('utf8')) def _generate_stylesheet(self): return self.STYLESHEET_TMPL #增加Tester显示 -Findyou def _generate_heading(self, report_attrs): a_lines = [] for name, value in report_attrs: line = self.HEADING_ATTRIBUTE_TMPL % dict( name = saxutils.escape(name), value = saxutils.escape(value), ) a_lines.append(line) heading = self.HEADING_TMPL % dict( title = saxutils.escape(self.title), parameters = ''.join(a_lines), description = saxutils.escape(self.description), tester= saxutils.escape(self.tester), ) return heading #生成报告 --Findyou添加注释 def _generate_report(self, result): rows = [] sortedResult = self.sortResult(result.result) for cid, (cls, cls_results) in enumerate(sortedResult): # subtotal for a class np = nf = ne = 0 for n,t,o,e in cls_results: if n == 0: np += 1 elif n == 1: nf += 1 else: ne += 1 # format class description if cls.__module__ == "__main__": name = cls.__name__ else: name = "%s.%s" % (cls.__module__, cls.__name__) doc = cls.__doc__ and cls.__doc__.split(" ")[0] or "" desc = doc and '%s: %s' % (name, doc) or name row = self.REPORT_CLASS_TMPL % dict( style = ne > 0 and 'warning' or nf > 0 and 'danger' or 'success', desc = desc, count = np+nf+ne, Pass = np, fail = nf, error = ne, cid = 'c%s' % (cid+1), ) rows.append(row) for tid, (n,t,o,e) in enumerate(cls_results): self._generate_report_test(rows, cid, tid, n, t, o, e) report = self.REPORT_TMPL % dict( test_list = ''.join(rows), count = str(result.success_count+result.failure_count+result.error_count), Pass = str(result.success_count), fail = str(result.failure_count), error = str(result.error_count), passrate =self.passrate, ) return report def _generate_report_test(self, rows, cid, tid, n, t, o, e): # e.g. 'pt1.1', 'ft1.1', etc has_output = bool(o or e) # ID修改点为下划线,支持Bootstrap折叠展开特效 - Findyou v0.8.2.1 #增加error分类 - Findyou v0.8.2.3 tid = (n == 0 and 'p' or n == 1 and 'f' or 'e') + 't%s_%s' % (cid + 1, tid + 1) name = t.id().split('.')[-1] doc = t.shortDescription() or "" desc = doc and ('%s: %s' % (name, doc)) or name tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL # utf-8 支持中文 - Findyou # o and e should be byte string because they are collected from stdout and stderr? if isinstance(o, str): # TODO: some problem with 'string_escape': it escape and mess up formating # uo = unicode(o.encode('string_escape')) try: uo = o except: uo = o.decode('utf-8') else: uo = o if isinstance(e, str): # TODO: some problem with 'string_escape': it escape and mess up formating # ue = unicode(e.encode('string_escape')) try: ue = e except: ue = e.decode('utf-8') else: ue = e script = self.REPORT_TEST_OUTPUT_TMPL % dict( id = tid, output = saxutils.escape(uo+ue), ) row = tmpl % dict( tid = tid, Class = (n == 0 and 'hiddenRow' or 'none'), style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'), desc = desc, script = script, status = self.STATUS[n], ) rows.append(row) if not has_output: return def _generate_ending(self): return self.ENDING_TMPL ############################################################################## # Facilities for running tests from the command line ############################################################################## # Note: Reuse unittest.TestProgram to launch test. In the future we may # build our own launcher to support more specific command line # parameters like test title, CSS, etc. class TestProgram(unittest.TestProgram): """ A variation of the unittest.TestProgram. Please refer to the base class for command line parameters. """ def runTests(self): # Pick HTMLTestReportCN as the default test runner. # base class's testRunner parameter is not useful because it means # we have to instantiate HTMLTestReportCN before we know self.verbosity. if self.testRunner is None: self.testRunner = HTMLTestReportCN(verbosity=self.verbosity) unittest.TestProgram.runTests(self) main = TestProgram ############################################################################## # Executing this module from the command line ############################################################################## if __name__ == "__main__": main(module=None)
【Config】
定义配置文件config.ini
# -*- coding: utf-8 -*- [HTTP] scheme = http baseurl = 127.0.0.1 port = 5000 timeout = 10.0 [DATABASE] host = 10.181.79.156 port = 3306 user = root passwd = root database = interface dbchar = utf8 table = interface_test [EMAIL] on_off = off EMAIL_SERVICE = smtp.qq.com EMAIL_PORT = 465 SENDER_ADDRESS = 364942727@qq.com SENDER_PASSWORD = szkaushkeanabcde RECEIVER_ADDRESS = 364942727@qq.com
对读取配置文件config.ini方法的封装
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:readConfig.py @E-mail:364942727@qq.com @Time:2020/9/3 13:58 上午 @Author:Nobita @Version:1.0 @Desciption:封装读取配置ini文件 """ import os import configparser import getpathInfo class ReadConfig(): def __init__(self): self.path = getpathInfo.get_Path() # 调用实例化 self.config_path = os.path.join(self.path, 'Config', 'Config.ini') # 这句话是在path路径下再加一级 self.config = configparser.ConfigParser() # 调用外部的读取配置文件的方法 self.config.read(self.config_path, encoding='utf-8') def get_http(self, name): value = self.config.get('HTTP', name) return value def get_email(self, name): value = self.config.get('EMAIL', name) return value def get_mysql(self, name): # 写好,留以后备用。但是因为我们没有对数据库的操作,所以这个可以屏蔽掉 value = self.config.get('DATABASE', name) return value if __name__ == '__main__': # 测试一下,我们读取配置文件的方法是否可用 print('HTTP中的baseurl值为:', ReadConfig().get_http('baseurl')) print('EMAIL中的开关on_off值为:', ReadConfig().get_email('on_off'))
定义接口用例是否执行的配置文件
learning-API-test/test_login learning-API-test/test_header #learning-API-test/test_auth #learning-API-test/test_menu
【learning-API-test】
写了一个用来测试的接口demo,具体代码参考github,这里不做详细介绍。
【Log】
文件夹logs用来存储log日志的文件
【框架流程图】
存放此接口自动化框架的流程图
【Report】
存放生成的html接口测试报告report.html
【TestCase】
用来存放各个接口的测试用例。这里我举两个栗子
/login接口
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:test_login.py @E-mail:364942727@qq.com @Time:2020/9/3 9:28 下午 @Author:Nobita @Version:1.0 @Desciption:/login接口的测试用例及断言 """ import json import unittest import paramunittest from Common import readExcel, geturlParams from Common.Assert import Assertions from Common.Request import RunMain url = geturlParams.geturlParams().get_Url() # 调用我们的geturlParams获取我们拼接的URL login_xls = readExcel.readExcel().get_xls('learning-API-test_Case.xlsx', 'login') @paramunittest.parametrized(*login_xls) class test_learning_API(unittest.TestCase): def setParameters(self, case_name, path, headers, data, method): """ set params :param case_name: :param path :param headers :param data :param method :return: """ self.case_name = case_name self.path = path self.headers = headers self.data = data self.method = method def description(self): """ test report description :return: """ print(self.case_name) def setUp(self): """ :return: """ print("测试开始,测试用例名称:{}".format(self.case_name)) def test_login(self): self.checkResult() def tearDown(self): print("测试结束,输出log完结 ") def checkResult(self): """ check test Log :return: """ request_url = url + self.path new_data = json.loads(self.data) # 将Excel中提取的data从字符串转换成字典形式入参 info = RunMain().run_main(method=self.method, url=request_url, data=new_data) # 根据Excel中的method调用run_main来进行requests请求,并拿到响应 print('接口响应报文:{}'.format(info)) # 在report中打印响应报文 # 对响应结果进行断言 if self.case_name == 'login_pass': Assertions().assert_code(info['code'], 10200) Assertions().assert_in_text(info['message'], 'success') if self.case_name == 'login_error': Assertions().assert_code(info['code'], 10104) Assertions().assert_in_text(info['message'], 'error') if self.case_name == 'login_null': Assertions().assert_code(info['code'], 10103) Assertions().assert_in_text(info['message'], 'null') if __name__ == "__main__": # unittest.main() pass
/header接口
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:test_header.py @E-mail:364942727@qq.com @Time:2020/9/3 11:28 下午 @Author:Nobita @Version:1.0 @Desciption:/headers接口的测试用例及断言 """ import json import unittest import paramunittest from Common import readExcel, geturlParams from Common.Assert import Assertions from Common.Request import RunMain url = geturlParams.geturlParams().get_Url() # 调用我们的geturlParams获取我们拼接的URL login_xls = readExcel.readExcel().get_xls('learning-API-test_Case.xlsx', 'header') @paramunittest.parametrized(*login_xls) class test_learning_API(unittest.TestCase): def setParameters(self, case_name, path, headers, data, method): """ set params :param case_name: :param path :param headers :param data :param method :return: """ self.case_name = case_name self.path = path self.headers = headers self.data = data self.method = method def description(self): """ test report description :return: """ print(self.case_name) def setUp(self): """ :return: """ print("测试开始,测试用例名称:{}".format(self.case_name)) def test_header(self): self.checkResult() def tearDown(self): print("测试结束,输出log完结 ") def checkResult(self): """ check test Log :return: """ request_url = url + self.path headers = self.headers new_headers = json.loads(headers) info = RunMain().run_main(method=self.method, url=request_url, headers=new_headers ) # 根据Excel中的method调用run_main来进行requests请求,并拿到响应 print('接口响应报文:{}'.format(info)) # 在report中打印响应报文 # 对响应结果进行断言 if self.case_name == 'header_pass': Assertions().assert_code(info['code'], 10200) Assertions().assert_in_text(info['message'], 'ok') if __name__ == "__main__": # unittest.main() pass
【TestFile】
用来存放接口项目的测试数据,具体内容参考github上的文件内容。
【getpathInfo】
用来获取项目的文件路径,一般都放在工程根目录。
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:getpathInfo.py @E-mail:364942727@qq.com @Time:2020/9/3 7:58 下午 @Author:Nobita @Version:1.0 @Desciption:获取项目的文件路径 """ import os def get_Path(): path = os.path.split(os.path.realpath(__file__))[0] return path if __name__ == '__main__': # 执行该文件,测试下是否OK print('测试路径是否OK,路径为:', get_Path())
【requirements.txt】
整个项目所需要的依赖包及精确的版本信息。
APScheduler==3.6.3 certifi==2020.6.20 chardet==3.0.4 click==7.1.2 Flask==1.0.2 idna==2.8 itsdangerous==1.1.0 Jinja2==2.11.2 MarkupSafe==1.1.1 ParamUnittest==0.2 pycryptodome==3.7.3 PyEmail==0.0.1 pytz==2020.1 requests==2.22.0 six==1.15.0 tzlocal==2.1 urllib3==1.25.10 Werkzeug==1.0.1 xlrd==1.2.0
【RunAll.py】
对项目所有功能模块调用的封装。
import os import Common.HTMLTestRunner as HTMLTestRunner import getpathInfo import unittest from Config import readConfig from Common.SendEmail import SendEmail from Common.Log import logger send_mail = SendEmail() path = getpathInfo.get_Path() report_path = os.path.join(path, 'Report') resultPath = os.path.join(report_path, "report.html") # Log/report.html on_off = readConfig.ReadConfig().get_email('on_off') log = logger class AllTest: # 定义一个类AllTest def __init__(self): # 初始化一些参数和数 self.caseListFile = os.path.join(path, "Config", "caselist.txt") # 配置执行哪些测试文件的配置文件路径 self.caseFile = os.path.join(path, "TestCase") # 真正的测试断言文件路径 self.caseList = [] log.info('测试报告的路径:{},执行用例配置文件路径:{}'.format(resultPath, self.caseListFile)) # 将文件路径输入到日志,方便定位查看问题 def set_case_list(self): """ 读取caselist.txt文件中的用例名称,并添加到caselist元素组 :return: """ fb = open(self.caseListFile) for value in fb.readlines(): data = str(value) if data != '' and not data.startswith("#"): # 如果data非空且不以#开头 self.caseList.append(data.replace(" ", "")) # 读取每行数据会将换行转换为 ,去掉每行数据中的 fb.close() log.info('执行的测试用例:{}'.format(self.caseList)) def set_case_suite(self): """ :return: """ self.set_case_list() # 通过set_case_list()拿到caselist元素组 test_suite = unittest.TestSuite() suite_module = [] for case in self.caseList: # 从caselist元素组中循环取出case case_name = case.split("/")[-1] # 通过split函数来将aaa/bbb分割字符串,-1取后面,0取前面 print(case_name + ".py") # 打印出取出来的名称 # 批量加载用例,第一个参数为用例存放路径,第一个参数为路径文件名 discover = unittest.defaultTestLoader.discover(self.caseFile, pattern=case_name + '.py', top_level_dir=None) suite_module.append(discover) # 将discover存入suite_module元素组 print('suite_module:' + str(suite_module)) if len(suite_module) > 0: # 判断suite_module元素组是否存在元素 for suite in suite_module: # 如果存在,循环取出元素组内容,命名为suite for test_name in suite: # 从discover中取出test_name,使用addTest添加到测试集 test_suite.addTest(test_name) else: print('else:') return None return test_suite # 返回测试集 def run(self): """ run test :return: """ try: suit = self.set_case_suite() # 调用set_case_suite获取test_suite if suit is not None: # 判断test_suite是否为空 fp = open(resultPath, 'wb') # 打开Report/report.html测试报告文件,如果不存在就创建 # 调用HTMLTestRunner runner = HTMLTestRunner.HTMLTestReportCN(stream=fp, tester='Shengkai Chen', title='Learning_API 接口测试报告', description=None) runner.run(suit) else: print("Have no case to test.") log.info('没有可以执行的测试用例,请查看用例配置文件caselist.txt') except Exception as ex: print(str(ex)) log.info('{}'.format(str(ex))) finally: print("*********TEST END*********") # 判断邮件发送的开关 if on_off == 'on': SendEmail().send_email() else: print("邮件发送开关配置关闭,请打开开关后可正常自动发送测试报告") if __name__ == '__main__': AllTest().run()
【README.md】
接口测试框架项目的详细介绍文档。具体内容参考github上的文件内容。
结束语
花了一个周末的时间,对以前的框架代码进行了优化,
更多功能需要结合生产上的业务需求进行开发挖掘。
学习和工作是一个循序渐进,不断肯定以及不断否定自我的过程。
希望我们能在此过程中像代码一样迭代自我,加油!
最后我把这个项目仓库命名为:API_Auto_Test