框架主要采用 python+unittest+requests+HTMLTestRunner ,工具用的是 pycharm

一、框架结构

图片1.jpg
在 pycharm 中创建项目,并在项目中创建上图那样的目录。目录结构介绍如下,详细的作用见在后续的文件中有注解和介绍

common 文件夹

  • configDb.py:这个文件主要编写数据库连接池的相关内容
  • configEmail.py:这个文件主要是配置发送邮件的主题、正文等,将测试报告发送并抄送到相关人邮箱的逻辑
  • configHttp.py:这个文件主要来通过 get、post、put、delete 等方法来进行 http请 求,并拿到请求响应
  • HTMLTestRunner.py:主要是生成测试报告相关
  • Log.py:调用该类的方法,用来打印生成日志

    result 文件夹

  • logs:生成的日志文件

  • report.html:生成的测试报告

    testCase 文件夹

  • test01case.py:读取 userCase.xlsx 中的用例,使用 unittest 来进行断言校验

    testFile 文件夹

  • userCase.xlsx:对下面 test_api.py 接口服务里的接口,设计了三条简单的测试用例,如参数为 null,参数不正确等

  • caselist.txt:配置将要执行 testCase 目录下的哪些用例文件,前加#代表不进行执行。当项目过于庞大,用例足够多的时候,我们可以通过这个开关,来确定本次执行哪些接口的哪些用例。
  • config.ini:数据库、邮箱、接口等的配置项,用于方便的调用读取。
  • getpathInfo.py:获取项目绝对路径
  • geturlParams.py:获取接口的 URL、参数、method 等
  • readConfig.py:读取配置文件的方法,并返回文件中内容
  • readExcel.py:读取 Excel 的方法
  • runAll.py:开始执行接口自动化,项目工程部署完毕后直接运行该文件即可
  • test_api.py:自己写的提供本地测试的接口服务
  • test_sql.py:测试数据库连接池的文件

    二、搭建测试接口服务

    自己写一个简单的接口服务,用来对这个框架的测试。打开 test_api.py,写入如下代码 ```python

    自己写的提供本地测试的接口服务

    import flask import json from flask import request

‘’’ flask: web框架,通过flask提供的装饰器@server.route()将普通函数转换为服 ‘’’

创建一个服务,把当前这个pyhton文件当成一个服务

server = flask.Flask(name)

@Serve.route() 可以将普通函数转变为服务,登录接口的路径、请求方式

@server.route(‘/login’, methods=[‘get’, ‘post’]) def login():

  1. # 获取通过url请求参数的数据
  2. username = request.values.get('name')
  3. # 获取url请求传的密码,明文
  4. pwd = request.values.get('pwd')
  5. # 判断用户名、密码都不为空
  6. if username and pwd:
  7. if username == 'xiaoming' and pwd == '111':
  8. resu = {'code': 200, 'message': '登录成功'}
  9. return json.dumps(resu, ensure_ascii=False) # 将字段转换为字符串
  10. else:
  11. resu = {'code': -1, 'message': '账号或密码错误'}
  12. return json.dumps(resu, ensure_ascii=False)
  13. else:
  14. resu = {'code': 10001, 'message': '参数不能为空'}
  15. return json.dumps(resu, ensure_ascii=False)

if name == ‘main‘: server.run(debug=True, port=8888, host=’127.0.0.1’)

  1. 执行 test_api.py,在浏览器中输入 http://127.0.0.1:8888/login?name=xiaoming&pwd=1111 回车,验证我们的接口服务是否正常。如果能返回接口信息,则证明接口服务OK
  2. <a name="JV6Ro"></a>
  3. ## 三、配置文件读取
  4. 前面我们已经通过 flask 这个 web 框架创建好了我们用于测试的接口服务,因此我们可以把这个接口抽出来一些参数放到配置文件,然后通过一个读取配置文件的方法,方便后续的使用。同样还有邮件的相关配置。<br />在 config.ini 文件中写入内容:
  5. ```python
  6. # -*- coding: utf-8 -*-
  7. [HTTP]
  8. scheme = http
  9. baseurl = 127.0.0.1
  10. port = 888
  11. timeout = 10.0
  12. [EMAIL]
  13. on_off = on;
  14. subject = 接口自动化测试报告
  15. app = Outlook
  16. addressee = 785714@qq.com
  17. cc = zhxxxa@mxxxd.com

在HTTP中,配置协议、baseURL,端口 和 超时时间
在邮件中,on_off 是设置的一个开关,on 打开,发送邮件,其他不发送邮件。subject 是邮件主题,addressee 是收件人,cc 是抄送人
在编写 readConfig.py 文件前,先写一个获取项目某路径下某文件绝对路径的一个方法。在 getpathInfo.py 中写入:

  1. # 获取项目绝对路径
  2. import os
  3. def get_Path():
  4. path = os.path.split(os.path.realpath(__file__))[0]
  5. return path
  6. if __name__ == '__main__':
  7. print("测试路径是否OK,路径为: ", get_Path())

写完后可以测试一下是否打印出了该项目的绝对路径。
打开 readConfig.py 文件,写入如下内容:

  1. # 读取配置文件的方法,并返回文件中内容
  2. import os
  3. import configparser
  4. # 引入自己写的获取路径的类
  5. import getpathInfo
  6. # 调用实例化,这个类返回的路径为 D:\pyhton\xmyInterfaceTest\testFile
  7. path = getpathInfo.get_Path()
  8. # 在path路径下再加一级,最后变成D:\pyhton\xmyInterfaceTest\testFile\config.ini
  9. config_path = os.path.join(path, 'config.ini')
  10. # 调用外部的读取配置文件的方法
  11. config = configparser.ConfigParser()
  12. config.read(config_path, encoding='utf-8')
  13. class ReadConfig:
  14. def get_http(self, name):
  15. value = config.get('HTTP', name)
  16. return value
  17. def get_email(self, name):
  18. value = config.get('EMAIL', name)
  19. return value
  20. def get_mysql(self, name):
  21. value = config.get('DATABASE', name)
  22. return value
  23. if __name__ == '__main__':
  24. print('HTTP中的baseurl值为:', ReadConfig().get_http('baseurl'))
  25. print('EMAIL中开关on_off值为:', ReadConfig().get_email('on_off'))

执行一下 readConfig.py,查看数据是否正确
图片2.jpg

四、读取 Excel 中的 case

配置文件写好了,接口我们也有了。因此根据我们的接口设计我们简单的几条用例
在 case 文件中,创建一个 userCase.xlsx,在 excel 中写一下信息:
图片3.jpg
接下来,我们要对这个 Excel 进行数据的读取操作。打开 readExcel.py 文件,写入以下信息:

  1. # 读取Excel的方法
  2. import os
  3. # 己定义的内部类,该类返回项目的绝对路径
  4. import getpathInfo
  5. # 调用读Excel的第三方库xlrd
  6. from xlrd import open_workbook
  7. # 拿到该项目所在的绝对路径
  8. path = getpathInfo.get_Path()
  9. class readExcel:
  10. # xls_name填写用例的Excel名称 sheet_name该Excel的sheet名称
  11. def get_xls(self, xls_name, sheet_name):
  12. cls = []
  13. # 获取用例文件路径
  14. xlsPath = os.path.join(path, 'case', xls_name)
  15. # 打开用例的excel
  16. file = open_workbook(xlsPath)
  17. # 打开sheet文件
  18. sheet = file.sheet_by_name(sheet_name)
  19. # 获取这个sheet文件的行数
  20. nrows = sheet.nrows
  21. for i in range(nrows):
  22. # 如果这个sheet的第i行的第一列不等于case_name那么我们把这行的数据添加到cls[]
  23. if sheet.row_values(i)[0] != u'case_name':
  24. cls.append(sheet.row_values(i))
  25. return cls
  26. if __name__ == '__main__':
  27. print(readExcel().get_xls('userCase.xlsx', 'Login'))
  28. print(readExcel().get_xls('userCase.xlsx', 'Login')[0][1])
  29. print(readExcel().get_xls('userCase.xlsx', 'Login')[1][2])

写完后,验证是否能正常读取 excel 数据
图片4.jpg :::info 这里有一个坑,因为是用 xlrd 来打开 excel 文件的,但最近 xlrd 更新到了2.0.1版本,只支持 .xls 文件,不支持 .xlsx 文件。因此要安装旧版 xlrd ::: 安装方法:在命令行中,运行 pip install xlrd==1.2.0 这样就可以了
图片5.jpg

五、requests 请求

配置文件、读取配置文件、用例、读取用例、接口服务的文件都写好了,接下来该写对某个接口进行 http 请求。
首先要用 pip install requests 来安装第三方库
图片6.jpg
在 common 下的 configHttp.py 文件中,写入如下内容:

  1. # 通过get、post、put、delete等方法来进行http请求,并拿到请求响应
  2. import requests
  3. import json
  4. class RunMain:
  5. # 定义一个方法,传入需要的参数url 和 data
  6. def send_post(self, url, data):
  7. # 因为要封装post方法,所以这里的url和data不能写死
  8. # 参数必须要按照url、data顺序写入
  9. result = requests.post(url=url, data=data).json()
  10. res = json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2)
  11. return res
  12. def send_get(self, url, data):
  13. result = requests.get(url=url, params=data).json()
  14. res = json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2)
  15. return res
  16. # 定义一个run_main方法,通过传过来的method来进行不同的get或post请求
  17. def run_main(self, method, url=None, data=None):
  18. result = None
  19. if method == 'post':
  20. result = self.send_post(url, data)
  21. elif method == 'get':
  22. result = self.send_get(url, data)
  23. else:
  24. print("method值有误")
  25. return result
  26. # 验证一下自己写的请求是否正确
  27. if __name__ == '__main__':
  28. result1 = RunMain().run_main('post', 'http://127.0.0.1:8888/login', {'name': 'xiaoming', 'pwd': '111'})
  29. result2 = RunMain().run_main('get', 'http://127.0.0.1:8888/login', 'name=xiaoming&pwd=111')
  30. print(result1)
  31. print(result2)

执行该文件,验证结果,如果 get 和 post 请求都返回登录成功,则表示OK

六、参数动态化

前面的内容都是通过写死的参数来进行 requests 请求的,因此要写一个类,用来分别获取那些写死的参数。在 geturlParams.py 文件中写入内容如下:

  1. # 获取接口的URL、参数、method等
  2. import readConfig as readConfig
  3. readconfig = readConfig.ReadConfig()
  4. # 定义一个类,将从配置文件中读取数据,并进行拼接
  5. class geturlParams:
  6. def get_url(self):
  7. new_url = readconfig.get_http('scheme') + '://' + readconfig.get_http('baseurl') + ':8888' + '/login' + '?'
  8. # logger.info('new_url' + new_url)
  9. return new_url
  10. if __name__ == '__main__':
  11. print(geturlParams().get_url())

执行后将会发现,拼接后的结果为:http://127.0.0.1:8888/login? 这个和我们的请求是一致的

七、unittest 断言

前面基础内容准备好后,开始写 unittest 断言测试 case 了,在 test01case.py 文件中,写入如下内容:

  1. # 读取userCase.xlsx中的用例,使用unittest来进行断言校验
  2. import json
  3. import unittest
  4. from common.configHttp import RunMain
  5. import paramunittest
  6. from geturlParams import geturlParams
  7. import urllib.parse
  8. import readExcel
  9. # import pythoncom
  10. # pythoncom.CoInitialize()
  11. # 调用我们的geturlParams 获取我们拼接的URL
  12. url = geturlParams().get_url()
  13. login_xls = readExcel.readExcel().get_xls('userCase.xlsx', 'login')
  14. @paramunittest.parametrized(*login_xls)
  15. class testUserLogin(unittest.TestCase):
  16. def setParameters(self, case_name, path, query, method):
  17. self.case_name = str(case_name)
  18. self.path = str(path)
  19. self.query = str(query)
  20. self.method = str(method)
  21. def description(self):
  22. self.case_name
  23. def setUp(self):
  24. print(self.case_name + "测试开始前准备")
  25. def test01case(self):
  26. self.checkResult()
  27. def tearDown(self):
  28. print("测试结束,输出log完结\n\n")
  29. def checkResult(self):
  30. # 拼接完整的请求url
  31. new_url = url + self.query
  32. # 将一个完整的URL中的name=&pwd=转换成{'name':'xxx','pwd':'bbb'}
  33. data1 = dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(new_url).query))
  34. # 根据Excel中的method调用run_main来进行requests请求,并拿到响应
  35. info = RunMain().run_main(self.method, url, data1)
  36. # 将响应转换成字典格式
  37. ss = json.loads(info)
  38. # 如果case_name是login,说明合法,返回code的值应该是200
  39. if self.case_name == 'login':
  40. self.assertEqual(ss['code'], 200)
  41. if self.case_name == 'login_error':
  42. self.assertEqual(ss['code'], -1)
  43. if self.case_name == 'login_null':
  44. self.assertEqual(ss['code'], 10001)

八、测试报告 HTMLTestRunner

在 HTMLTestRunner.py 文件中,写入:

  1. # -*- coding: utf-8 -*-
  2. """
  3. A TestRunner for use with the Python unit testing framework. It
  4. generates a HTML report to show the result at a glance.
  5. The simplest way to use this is to invoke its main method. E.g.
  6. import unittest
  7. import HTMLTestRunner
  8. ... define your tests ...
  9. if __name__ == '__main__':
  10. HTMLTestRunner.main()
  11. For more customization options, instantiates a HTMLTestRunner object.
  12. HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
  13. # output to a file
  14. fp = file('my_report.html', 'wb')
  15. runner = HTMLTestRunner.HTMLTestRunner(
  16. stream=fp,
  17. title='My unit test',
  18. description='This demonstrates the report output by HTMLTestRunner.'
  19. )
  20. # Use an external stylesheet.
  21. # See the Template_mixin class for more customizable options
  22. runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
  23. # run the test
  24. runner.run(my_test_suite)
  25. ------------------------------------------------------------------------
  26. Copyright (c) 2004-2007, Wai Yip Tung
  27. All rights reserved.
  28. Redistribution and use in source and binary forms, with or without
  29. modification, are permitted provided that the following conditions are
  30. met:
  31. * Redistributions of source code must retain the above copyright notice,
  32. this list of conditions and the following disclaimer.
  33. * Redistributions in binary form must reproduce the above copyright
  34. notice, this list of conditions and the following disclaimer in the
  35. documentation and/or other materials provided with the distribution.
  36. * Neither the name Wai Yip Tung nor the names of its contributors may be
  37. used to endorse or promote products derived from this software without
  38. specific prior written permission.
  39. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  40. IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  41. TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
  42. PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
  43. OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  44. EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  45. PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  46. PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  47. LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  48. NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  49. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  50. """
  51. # URL: http://tungwaiyip.info/software/HTMLTestRunner.html
  52. __author__ = "Wai Yip Tung"
  53. __version__ = "0.9.1"
  54. """
  55. Change History
  56. Version 0.9.1
  57. * 用Echarts添加执行情况统计图 (灰蓝)
  58. Version 0.9.0
  59. * 改成Python 3.x (灰蓝)
  60. Version 0.8.3
  61. * 使用 Bootstrap稍加美化 (灰蓝)
  62. * 改为中文 (灰蓝)
  63. Version 0.8.2
  64. * Show output inline instead of popup window (Viorel Lupu).
  65. Version in 0.8.1
  66. * Validated XHTML (Wolfgang Borgert).
  67. * Added description of test classes and test cases.
  68. Version in 0.8.0
  69. * Define Template_mixin class for customization.
  70. * Workaround a IE 6 bug that it does not treat <script> block as CDATA.
  71. Version in 0.7.1
  72. * Back port to Python 2.3 (Frank Horowitz).
  73. * Fix missing scroll bars in detail log (Podi).
  74. """
  75. # TODO: color stderr
  76. # TODO: simplify javascript using ,ore than 1 class in the class attribute?
  77. import datetime
  78. import sys
  79. import io
  80. import time
  81. import unittest
  82. from xml.sax import saxutils
  83. # ------------------------------------------------------------------------
  84. # The redirectors below are used to capture output during testing. Output
  85. # sent to sys.stdout and sys.stderr are automatically captured. However
  86. # in some cases sys.stdout is already cached before HTMLTestRunner is
  87. # invoked (e.g. calling logging.basicConfig). In order to capture those
  88. # output, use the redirectors for the cached stream.
  89. #
  90. # e.g.
  91. # >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
  92. # >>>
  93. class OutputRedirector(object):
  94. """ Wrapper to redirect stdout or stderr """
  95. def __init__(self, fp):
  96. self.fp = fp
  97. def write(self, s):
  98. self.fp.write(s)
  99. def writelines(self, lines):
  100. self.fp.writelines(lines)
  101. def flush(self):
  102. self.fp.flush()
  103. stdout_redirector = OutputRedirector(sys.stdout)
  104. stderr_redirector = OutputRedirector(sys.stderr)
  105. # ----------------------------------------------------------------------
  106. # Template
  107. class Template_mixin(object):
  108. """
  109. Define a HTML template for report customerization and generation.
  110. Overall structure of an HTML report
  111. HTML
  112. +------------------------+
  113. |<html> |
  114. | <head> |
  115. | |
  116. | STYLESHEET |
  117. | +----------------+ |
  118. | | | |
  119. | +----------------+ |
  120. | |
  121. | </head> |
  122. | |
  123. | <body> |
  124. | |
  125. | HEADING |
  126. | +----------------+ |
  127. | | | |
  128. | +----------------+ |
  129. | |
  130. | REPORT |
  131. | +----------------+ |
  132. | | | |
  133. | +----------------+ |
  134. | |
  135. | ENDING |
  136. | +----------------+ |
  137. | | | |
  138. | +----------------+ |
  139. | |
  140. | </body> |
  141. |</html> |
  142. +------------------------+
  143. """
  144. STATUS = {
  145. 0: u'通过',
  146. 1: u'失败',
  147. 2: u'错误',
  148. }
  149. DEFAULT_TITLE = 'Unit Test Report'
  150. DEFAULT_DESCRIPTION = ''
  151. # ------------------------------------------------------------------------
  152. # HTML Template
  153. HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
  154. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  155. <html xmlns="http://www.w3.org/1999/xhtml">
  156. <head>
  157. <title>%(title)s</title>
  158. <meta name="generator" content="%(generator)s"/>
  159. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  160. <link href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet">
  161. <script src="https://cdn.bootcss.com/echarts/3.8.5/echarts.common.min.js"></script>
  162. <!-- <script type="text/javascript" src="js/echarts.common.min.js"></script> -->
  163. %(stylesheet)s
  164. </head>
  165. <body>
  166. <script language="javascript" type="text/javascript"><!--
  167. output_list = Array();
  168. /* level - 0:Summary; 1:Failed; 2:All */
  169. function showCase(level) {
  170. trs = document.getElementsByTagName("tr");
  171. for (var i = 0; i < trs.length; i++) {
  172. tr = trs[i];
  173. id = tr.id;
  174. if (id.substr(0,2) == 'ft') {
  175. if (level < 1) {
  176. tr.className = 'hiddenRow';
  177. }
  178. else {
  179. tr.className = '';
  180. }
  181. }
  182. if (id.substr(0,2) == 'pt') {
  183. if (level > 1) {
  184. tr.className = '';
  185. }
  186. else {
  187. tr.className = 'hiddenRow';
  188. }
  189. }
  190. }
  191. }
  192. function showClassDetail(cid, count) {
  193. var id_list = Array(count);
  194. var toHide = 1;
  195. for (var i = 0; i < count; i++) {
  196. tid0 = 't' + cid.substr(1) + '.' + (i+1);
  197. tid = 'f' + tid0;
  198. tr = document.getElementById(tid);
  199. if (!tr) {
  200. tid = 'p' + tid0;
  201. tr = document.getElementById(tid);
  202. }
  203. id_list[i] = tid;
  204. if (tr.className) {
  205. toHide = 0;
  206. }
  207. }
  208. for (var i = 0; i < count; i++) {
  209. tid = id_list[i];
  210. if (toHide) {
  211. document.getElementById('div_'+tid).style.display = 'none'
  212. document.getElementById(tid).className = 'hiddenRow';
  213. }
  214. else {
  215. document.getElementById(tid).className = '';
  216. }
  217. }
  218. }
  219. function showTestDetail(div_id){
  220. var details_div = document.getElementById(div_id)
  221. var displayState = details_div.style.display
  222. // alert(displayState)
  223. if (displayState != 'block' ) {
  224. displayState = 'block'
  225. details_div.style.display = 'block'
  226. }
  227. else {
  228. details_div.style.display = 'none'
  229. }
  230. }
  231. function html_escape(s) {
  232. s = s.replace(/&/g,'&amp;');
  233. s = s.replace(/</g,'&lt;');
  234. s = s.replace(/>/g,'&gt;');
  235. return s;
  236. }
  237. /* obsoleted by detail in <div>
  238. function showOutput(id, name) {
  239. var w = window.open("", //url
  240. name,
  241. "resizable,scrollbars,status,width=800,height=450");
  242. d = w.document;
  243. d.write("<pre>");
  244. d.write(html_escape(output_list[id]));
  245. d.write("\n");
  246. d.write("<a href='javascript:window.close()'>close</a>\n");
  247. d.write("</pre>\n");
  248. d.close();
  249. }
  250. */
  251. --></script>
  252. <div id="div_base">
  253. %(heading)s
  254. %(report)s
  255. %(ending)s
  256. %(chart_script)s
  257. </div>
  258. </body>
  259. </html>
  260. """ # variables: (title, generator, stylesheet, heading, report, ending, chart_script)
  261. ECHARTS_SCRIPT = """
  262. <script type="text/javascript">
  263. // 基于准备好的dom,初始化echarts实例
  264. var myChart = echarts.init(document.getElementById('chart'));
  265. // 指定图表的配置项和数据
  266. var option = {
  267. title : {
  268. text: '测试执行情况',
  269. x:'center'
  270. },
  271. tooltip : {
  272. trigger: 'item',
  273. formatter: "{a} <br/>{b} : {c} ({d}%%)"
  274. },
  275. color: ['#95b75d', 'grey', '#b64645'],
  276. legend: {
  277. orient: 'vertical',
  278. left: 'left',
  279. data: ['通过','失败','错误']
  280. },
  281. series : [
  282. {
  283. name: '测试执行情况',
  284. type: 'pie',
  285. radius : '60%%',
  286. center: ['50%%', '60%%'],
  287. data:[
  288. {value:%(Pass)s, name:'通过'},
  289. {value:%(fail)s, name:'失败'},
  290. {value:%(error)s, name:'错误'}
  291. ],
  292. itemStyle: {
  293. emphasis: {
  294. shadowBlur: 10,
  295. shadowOffsetX: 0,
  296. shadowColor: 'rgba(0, 0, 0, 0.5)'
  297. }
  298. }
  299. }
  300. ]
  301. };
  302. // 使用刚指定的配置项和数据显示图表。
  303. myChart.setOption(option);
  304. </script>
  305. """ # variables: (Pass, fail, error)
  306. # ------------------------------------------------------------------------
  307. # Stylesheet
  308. #
  309. # alternatively use a <link> for external style sheet, e.g.
  310. # <link rel="stylesheet" href="$url" type="text/css">
  311. STYLESHEET_TMPL = """
  312. <style type="text/css" media="screen">
  313. body { font-family: Microsoft YaHei,Consolas,arial,sans-serif; font-size: 80%; }
  314. table { font-size: 100%; }
  315. pre { white-space: pre-wrap;word-wrap: break-word; }
  316. /* -- heading ---------------------------------------------------------------------- */
  317. h1 {
  318. font-size: 16pt;
  319. color: gray;
  320. }
  321. .heading {
  322. margin-top: 0ex;
  323. margin-bottom: 1ex;
  324. }
  325. .heading .attribute {
  326. margin-top: 1ex;
  327. margin-bottom: 0;
  328. }
  329. .heading .description {
  330. margin-top: 2ex;
  331. margin-bottom: 3ex;
  332. }
  333. /* -- css div popup ------------------------------------------------------------------------ */
  334. a.popup_link {
  335. }
  336. a.popup_link:hover {
  337. color: red;
  338. }
  339. .popup_window {
  340. display: none;
  341. position: relative;
  342. left: 0px;
  343. top: 0px;
  344. /*border: solid #627173 1px; */
  345. padding: 10px;
  346. /*background-color: #E6E6D6; */
  347. font-family: "Lucida Console", "Courier New", Courier, monospace;
  348. text-align: left;
  349. font-size: 8pt;
  350. /* width: 500px;*/
  351. }
  352. }
  353. /* -- report ------------------------------------------------------------------------ */
  354. #show_detail_line {
  355. margin-top: 3ex;
  356. margin-bottom: 1ex;
  357. }
  358. #result_table {
  359. width: 99%;
  360. }
  361. #header_row {
  362. font-weight: bold;
  363. color: #303641;
  364. background-color: #ebebeb;
  365. }
  366. #total_row { font-weight: bold; }
  367. .passClass { background-color: #bdedbc; }
  368. .failClass { background-color: #ffefa4; }
  369. .errorClass { background-color: #ffc9c9; }
  370. .passCase { color: #6c6; }
  371. .failCase { color: #FF6600; font-weight: bold; }
  372. .errorCase { color: #c00; font-weight: bold; }
  373. .hiddenRow { display: none; }
  374. .testcase { margin-left: 2em; }
  375. /* -- ending ---------------------------------------------------------------------- */
  376. #ending {
  377. }
  378. #div_base {
  379. position:absolute;
  380. top:0%;
  381. left:5%;
  382. right:5%;
  383. width: auto;
  384. height: auto;
  385. margin: -15px 0 0 0;
  386. }
  387. </style>
  388. """
  389. # ------------------------------------------------------------------------
  390. # Heading
  391. #
  392. HEADING_TMPL = """
  393. <div class='page-header'>
  394. <h1>%(title)s</h1>
  395. %(parameters)s
  396. </div>
  397. <div style="float: left;width:50%%;"><p class='description'>%(description)s</p></div>
  398. <div id="chart" style="width:50%%;height:400px;float:left;"></div>
  399. """ # variables: (title, parameters, description)
  400. HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
  401. """ # variables: (name, value)
  402. # ------------------------------------------------------------------------
  403. # Report
  404. #
  405. REPORT_TMPL = u"""
  406. <div class="btn-group btn-group-sm">
  407. <button class="btn btn-default" onclick='javascript:showCase(0)'>总结</button>
  408. <button class="btn btn-default" onclick='javascript:showCase(1)'>失败</button>
  409. <button class="btn btn-default" onclick='javascript:showCase(2)'>全部</button>
  410. </div>
  411. <p></p>
  412. <table id='result_table' class="table table-bordered">
  413. <colgroup>
  414. <col align='left' />
  415. <col align='right' />
  416. <col align='right' />
  417. <col align='right' />
  418. <col align='right' />
  419. <col align='right' />
  420. </colgroup>
  421. <tr id='header_row'>
  422. <td>测试套件/测试用例</td>
  423. <td>总数</td>
  424. <td>通过</td>
  425. <td>失败</td>
  426. <td>错误</td>
  427. <td>查看</td>
  428. </tr>
  429. %(test_list)s
  430. <tr id='total_row'>
  431. <td>总计</td>
  432. <td>%(count)s</td>
  433. <td>%(Pass)s</td>
  434. <td>%(fail)s</td>
  435. <td>%(error)s</td>
  436. <td>&nbsp;</td>
  437. </tr>
  438. </table>
  439. """ # variables: (test_list, count, Pass, fail, error)
  440. REPORT_CLASS_TMPL = u"""
  441. <tr class='%(style)s'>
  442. <td>%(desc)s</td>
  443. <td>%(count)s</td>
  444. <td>%(Pass)s</td>
  445. <td>%(fail)s</td>
  446. <td>%(error)s</td>
  447. <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">详情</a></td>
  448. </tr>
  449. """ # variables: (style, desc, count, Pass, fail, error, cid)
  450. REPORT_TEST_WITH_OUTPUT_TMPL = r"""
  451. <tr id='%(tid)s' class='%(Class)s'>
  452. <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
  453. <td colspan='5' align='center'>
  454. <!--css div popup start-->
  455. <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
  456. %(status)s</a>
  457. <div id='div_%(tid)s' class="popup_window">
  458. <pre>%(script)s</pre>
  459. </div>
  460. <!--css div popup end-->
  461. </td>
  462. </tr>
  463. """ # variables: (tid, Class, style, desc, status)
  464. REPORT_TEST_NO_OUTPUT_TMPL = r"""
  465. <tr id='%(tid)s' class='%(Class)s'>
  466. <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
  467. <td colspan='5' align='center'>%(status)s</td>
  468. </tr>
  469. """ # variables: (tid, Class, style, desc, status)
  470. REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s""" # variables: (id, output)
  471. # ------------------------------------------------------------------------
  472. # ENDING
  473. #
  474. ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""
  475. # -------------------- The end of the Template class -------------------
  476. TestResult = unittest.TestResult
  477. class _TestResult(TestResult):
  478. # note: _TestResult is a pure representation of results.
  479. # It lacks the output and reporting ability compares to unittest._TextTestResult.
  480. def __init__(self, verbosity=1):
  481. TestResult.__init__(self)
  482. self.stdout0 = None
  483. self.stderr0 = None
  484. self.success_count = 0
  485. self.failure_count = 0
  486. self.error_count = 0
  487. self.verbosity = verbosity
  488. # result is a list of result in 4 tuple
  489. # (
  490. # result code (0: success; 1: fail; 2: error),
  491. # TestCase object,
  492. # Test output (byte string),
  493. # stack trace,
  494. # )
  495. self.result = []
  496. self.subtestlist = []
  497. def startTest(self, test):
  498. TestResult.startTest(self, test)
  499. # just one buffer for both stdout and stderr
  500. self.outputBuffer = io.StringIO()
  501. stdout_redirector.fp = self.outputBuffer
  502. stderr_redirector.fp = self.outputBuffer
  503. self.stdout0 = sys.stdout
  504. self.stderr0 = sys.stderr
  505. sys.stdout = stdout_redirector
  506. sys.stderr = stderr_redirector
  507. def complete_output(self):
  508. """
  509. Disconnect output redirection and return buffer.
  510. Safe to call multiple times.
  511. """
  512. if self.stdout0:
  513. sys.stdout = self.stdout0
  514. sys.stderr = self.stderr0
  515. self.stdout0 = None
  516. self.stderr0 = None
  517. return self.outputBuffer.getvalue()
  518. def stopTest(self, test):
  519. # Usually one of addSuccess, addError or addFailure would have been called.
  520. # But there are some path in unittest that would bypass this.
  521. # We must disconnect stdout in stopTest(), which is guaranteed to be called.
  522. self.complete_output()
  523. def addSuccess(self, test):
  524. if test not in self.subtestlist:
  525. self.success_count += 1
  526. TestResult.addSuccess(self, test)
  527. output = self.complete_output()
  528. self.result.append((0, test, output, ''))
  529. if self.verbosity > 1:
  530. sys.stderr.write('ok ')
  531. sys.stderr.write(str(test))
  532. sys.stderr.write('\n')
  533. else:
  534. sys.stderr.write('.')
  535. def addError(self, test, err):
  536. self.error_count += 1
  537. TestResult.addError(self, test, err)
  538. _, _exc_str = self.errors[-1]
  539. output = self.complete_output()
  540. self.result.append((2, test, output, _exc_str))
  541. if self.verbosity > 1:
  542. sys.stderr.write('E ')
  543. sys.stderr.write(str(test))
  544. sys.stderr.write('\n')
  545. else:
  546. sys.stderr.write('E')
  547. def addFailure(self, test, err):
  548. self.failure_count += 1
  549. TestResult.addFailure(self, test, err)
  550. _, _exc_str = self.failures[-1]
  551. output = self.complete_output()
  552. self.result.append((1, test, output, _exc_str))
  553. if self.verbosity > 1:
  554. sys.stderr.write('F ')
  555. sys.stderr.write(str(test))
  556. sys.stderr.write('\n')
  557. else:
  558. sys.stderr.write('F')
  559. def addSubTest(self, test, subtest, err):
  560. if err is not None:
  561. if getattr(self, 'failfast', False):
  562. self.stop()
  563. if issubclass(err[0], test.failureException):
  564. self.failure_count += 1
  565. errors = self.failures
  566. errors.append((subtest, self._exc_info_to_string(err, subtest)))
  567. output = self.complete_output()
  568. self.result.append((1, test, output + '\nSubTestCase Failed:\n' + str(subtest),
  569. self._exc_info_to_string(err, subtest)))
  570. if self.verbosity > 1:
  571. sys.stderr.write('F ')
  572. sys.stderr.write(str(subtest))
  573. sys.stderr.write('\n')
  574. else:
  575. sys.stderr.write('F')
  576. else:
  577. self.error_count += 1
  578. errors = self.errors
  579. errors.append((subtest, self._exc_info_to_string(err, subtest)))
  580. output = self.complete_output()
  581. self.result.append(
  582. (2, test, output + '\nSubTestCase Error:\n' + str(subtest), self._exc_info_to_string(err, subtest)))
  583. if self.verbosity > 1:
  584. sys.stderr.write('E ')
  585. sys.stderr.write(str(subtest))
  586. sys.stderr.write('\n')
  587. else:
  588. sys.stderr.write('E')
  589. self._mirrorOutput = True
  590. else:
  591. self.subtestlist.append(subtest)
  592. self.subtestlist.append(test)
  593. self.success_count += 1
  594. output = self.complete_output()
  595. self.result.append((0, test, output + '\nSubTestCase Pass:\n' + str(subtest), ''))
  596. if self.verbosity > 1:
  597. sys.stderr.write('ok ')
  598. sys.stderr.write(str(subtest))
  599. sys.stderr.write('\n')
  600. else:
  601. sys.stderr.write('.')
  602. class HTMLTestRunner(Template_mixin):
  603. def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
  604. self.stream = stream
  605. self.verbosity = verbosity
  606. if title is None:
  607. self.title = self.DEFAULT_TITLE
  608. else:
  609. self.title = title
  610. if description is None:
  611. self.description = self.DEFAULT_DESCRIPTION
  612. else:
  613. self.description = description
  614. self.startTime = datetime.datetime.now()
  615. def run(self, test):
  616. "Run the given test case or test suite."
  617. result = _TestResult(self.verbosity)
  618. test(result)
  619. self.stopTime = datetime.datetime.now()
  620. self.generateReport(test, result)
  621. print('\nTime Elapsed: %s' % (self.stopTime - self.startTime), file=sys.stderr)
  622. return result
  623. def sortResult(self, result_list):
  624. # unittest does not seems to run in any particular order.
  625. # Here at least we want to group them together by class.
  626. rmap = {}
  627. classes = []
  628. for n, t, o, e in result_list:
  629. cls = t.__class__
  630. if cls not in rmap:
  631. rmap[cls] = []
  632. classes.append(cls)
  633. rmap[cls].append((n, t, o, e))
  634. r = [(cls, rmap[cls]) for cls in classes]
  635. return r
  636. def getReportAttributes(self, result):
  637. """
  638. Return report attributes as a list of (name, value).
  639. Override this to add custom attributes.
  640. """
  641. startTime = str(self.startTime)[:19]
  642. duration = str(self.stopTime - self.startTime)
  643. status = []
  644. if result.success_count: status.append(u'通过 %s' % result.success_count)
  645. if result.failure_count: status.append(u'失败 %s' % result.failure_count)
  646. if result.error_count: status.append(u'错误 %s' % result.error_count)
  647. if status:
  648. status = ' '.join(status)
  649. else:
  650. status = 'none'
  651. return [
  652. (u'开始时间', startTime),
  653. (u'运行时长', duration),
  654. (u'状态', status),
  655. ]
  656. def generateReport(self, test, result):
  657. report_attrs = self.getReportAttributes(result)
  658. generator = 'HTMLTestRunner %s' % __version__
  659. stylesheet = self._generate_stylesheet()
  660. heading = self._generate_heading(report_attrs)
  661. report = self._generate_report(result)
  662. ending = self._generate_ending()
  663. chart = self._generate_chart(result)
  664. output = self.HTML_TMPL % dict(
  665. title=saxutils.escape(self.title),
  666. generator=generator,
  667. stylesheet=stylesheet,
  668. heading=heading,
  669. report=report,
  670. ending=ending,
  671. chart_script=chart
  672. )
  673. self.stream.write(output.encode('utf8'))
  674. # self.stream.write(output)
  675. def _generate_stylesheet(self):
  676. return self.STYLESHEET_TMPL
  677. def _generate_heading(self, report_attrs):
  678. a_lines = []
  679. for name, value in report_attrs:
  680. line = self.HEADING_ATTRIBUTE_TMPL % dict(
  681. name=saxutils.escape(name),
  682. value=saxutils.escape(value),
  683. )
  684. a_lines.append(line)
  685. heading = self.HEADING_TMPL % dict(
  686. title=saxutils.escape(self.title),
  687. parameters=''.join(a_lines),
  688. description=saxutils.escape(self.description),
  689. )
  690. return heading
  691. def _generate_report(self, result):
  692. rows = []
  693. sortedResult = self.sortResult(result.result)
  694. for cid, (cls, cls_results) in enumerate(sortedResult):
  695. # subtotal for a class
  696. np = nf = ne = 0
  697. for n, t, o, e in cls_results:
  698. if n == 0:
  699. np += 1
  700. elif n == 1:
  701. nf += 1
  702. else:
  703. ne += 1
  704. # format class description
  705. if cls.__module__ == "__main__":
  706. name = cls.__name__
  707. else:
  708. name = "%s.%s" % (cls.__module__, cls.__name__)
  709. doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
  710. desc = doc and '%s: %s' % (name, doc) or name
  711. row = self.REPORT_CLASS_TMPL % dict(
  712. style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
  713. desc=desc,
  714. count=np + nf + ne,
  715. Pass=np,
  716. fail=nf,
  717. error=ne,
  718. cid='c%s' % (cid + 1),
  719. )
  720. rows.append(row)
  721. for tid, (n, t, o, e) in enumerate(cls_results):
  722. self._generate_report_test(rows, cid, tid, n, t, o, e)
  723. report = self.REPORT_TMPL % dict(
  724. test_list=''.join(rows),
  725. count=str(result.success_count + result.failure_count + result.error_count),
  726. Pass=str(result.success_count),
  727. fail=str(result.failure_count),
  728. error=str(result.error_count),
  729. )
  730. return report
  731. def _generate_chart(self, result):
  732. chart = self.ECHARTS_SCRIPT % dict(
  733. Pass=str(result.success_count),
  734. fail=str(result.failure_count),
  735. error=str(result.error_count),
  736. )
  737. return chart
  738. def _generate_report_test(self, rows, cid, tid, n, t, o, e):
  739. # e.g. 'pt1.1', 'ft1.1', etc
  740. has_output = bool(o or e)
  741. tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid + 1, tid + 1)
  742. name = t.id().split('.')[-1]
  743. doc = t.shortDescription() or ""
  744. desc = doc and ('%s: %s' % (name, doc)) or name
  745. tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
  746. script = self.REPORT_TEST_OUTPUT_TMPL % dict(
  747. id=tid,
  748. output=saxutils.escape(o + e),
  749. )
  750. row = tmpl % dict(
  751. tid=tid,
  752. Class=(n == 0 and 'hiddenRow' or 'none'),
  753. style=(n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none')),
  754. desc=desc,
  755. script=script,
  756. status=self.STATUS[n],
  757. )
  758. rows.append(row)
  759. if not has_output:
  760. return
  761. def _generate_ending(self):
  762. return self.ENDING_TMPL
  763. ##############################################################################
  764. # Facilities for running tests from the command line
  765. ##############################################################################
  766. # Note: Reuse unittest.TestProgram to launch test. In the future we may
  767. # build our own launcher to support more specific command line
  768. # parameters like test title, CSS, etc.
  769. class TestProgram(unittest.TestProgram):
  770. """
  771. A variation of the unittest.TestProgram. Please refer to the base
  772. class for command line parameters.
  773. """
  774. def runTests(self):
  775. # Pick HTMLTestRunner as the default test runner.
  776. # base class's testRunner parameter is not useful because it means
  777. # we have to instantiate HTMLTestRunner before we know self.verbosity.
  778. if self.testRunner is None:
  779. self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
  780. unittest.TestProgram.runTests(self)
  781. main = TestProgram
  782. ##############################################################################
  783. # Executing this module from the command line
  784. ##############################################################################
  785. if __name__ == "__main__":
  786. main(module=None)

九、调用生成测试报告

用例控制 unittest

先别急着创建 runAll.py 文件,在创建此文件前,还缺少点东东。先打开 caselist.txt 文件,写入内容如下:

  1. user/test01case
  2. #user/test02case
  3. #user/test03case
  4. #user/test04case
  5. #user/test05case
  6. #shop/test_shop_list
  7. #shop/test_my_shop
  8. #shop/test_new_shop

通过这个文件来控制执行哪些模块下的哪些 unittest 用例文件。如在实际的项目中:use 模块下的 test01case.py,店铺 shop 模块下的我的店铺 my_shop

发送邮件 configEmail.py

但目前还缺少一个发送邮件的文件。打开 configEmail.py 文件,写入内容如下:

# 主要是配置发送邮件的主题、正文
import os
import smtplib
import base64
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart


class SendEmail(object):
    # email_host 是本地邮件发送邮件的服务器地址(SMTP),要自己去检查
    # ssl_port 是本地邮件发送的服务器的ssl端口
    def __init__(self, username, passwd, recv, title, content,
                 file=None, ssl=False,
                 email_host='mail.xxa.com', port=25, ssl_port=994):
        # 用户名
        self.username = username
        # 密码
        self.passwd = passwd
        # 收件人,多个要穿List['aa@qq.com','bb@qq.com']
        self.recv = recv
        # 邮件标题
        self.title = title
        # 邮件正文
        self.content = content
        # 邮件路径,如果不在当前目录下,要写绝对路径
        self.file = file
        # smtp服务器地址
        self.email_host = email_host
        # 普通端口
        self.port = port
        # 是否安全链接
        self.ssl = ssl
        # 安全链接端口
        self.ssl_port = ssl_port

    def send_email(self):
        # 发送内容对象
        msg = MIMEMultipart()
        # 处理附件
        if self.file:
            # 只读文件名,不取路径
            file_name = os.path.split(self.file)[-1]
            try:
                f = open(self.file, 'rb').read()
            except Exception as e:
                raise Exception('附件打不开!!!')
            else:
                att = MIMEText(f, "base64", "utf-8")
                att["Content-Type"] = 'application/octet-stream'
                # base64.b64encode(file_name.encode()).decode()
                new_file_name = '=?utf-8?b?' + base64.b64encode(file_name.encode()).decode() + '?='
                # 这里是处理文件名为中文的,必须这么写
                att["Content-Disposition"] = 'attachment; filename="%s"' % (new_file_name)
                msg.attach(att)
        # 邮件正文内容
        msg.attach(MIMEText(self.content))
        # 邮件主题
        msg['Subject'] = self.title
        # 发送者账号
        msg['From'] = self.username
        # 接收者账号列表
        msg['To'] = ','.join(self.recv)
        if self.ssl:
            self.smtp = smtplib.SMTP_SSL(self.email_host, port=self.ssl_port)
        else:
            self.smtp = smtplib.SMTP(self.email_host, port=self.port)
        # 发送邮件服务器的对象
        self.smtp.login(self.username, self.passwd)
        try:
            self.smtp.sendmail(self.username, self.recv, msg.as_string())
            pass
        except Exception as e:
            print("出错了。。。", e)
        else:
            print("邮件发送成功")
        self.smtp.quit()


if __name__ == '__main__':
    m = SendEmail(
        # 邮箱账号
        username='zhxxxwx8@mxxxxud.com',
        # 邮箱密码
        passwd='',
        # 收件人邮箱
        recv=[''],
        title='测试自动发送邮件',
        content='测试发送邮件',
        file=r'E:\blog\image1.jpg',
        ssl=True,
    )
    m.send_email()

这里要再次说明,email_host 和 ssl_port 和本地使用的邮箱有关系,我这里用的是 outlook,因此在 outlook 的客户端中查看了发送邮件的服务器地址和端口
运行 configEmail.py 验证邮件发送是否正确
图片7.jpg

执行所有文件 runAll.py

# 开始执行接口自动化,项目工程部署完毕后直接运行该文件即可
import os
import unittest
import common.HTMLTestRunner as HTMLTestRunner
import getpathInfo
import readConfig
from common.configEmail import SendEmail
# import common.Log

send_mail = SendEmail(
    username='zxxxx8@mxxxxd.com',
    passwd='',
    recv=[''],
    title='测试报告',
    content='测试报告',
    file=r'D:\pyhton\xmyInterfaceTest\result\report.html',
    ssl=True,
)
path = getpathInfo.get_Path()
report_path = os.path.join(path, 'result')
on_off = readConfig.ReadConfig().get_email('on_off')
# log = common.Log.logger


class AllTest:
    # 初始化一些参数和数据
    def __init__(self):
        global resultPath
        # result/report.html
        resultPath = os.path.join(report_path, "report.html")
        # 配置执行哪些测试文件的配置文件路径
        self.caseListFile = os.path.join(path, "caselist.txt")
        # 真正的测试断言文件路径
        self.caseFile = os.path.join(path, "testCase")
        self.caseList = []

        # 将resultPath的值输出到日志中,方便定位问题
        # log.info('resultPath: ' + resultPath)
        # log.info('caseListFile: ' + self.caseListFile)
        # log.info('caseList: %s', self.caseList)

    def set_case_list(self):
        """
        取caselist.txt文件中的用例名称,并添加到caselist元素组
        :return:
        """
        fb = open(self.caseListFile)
        for value in fb.readlines():
            data = str(value)
            # 如果data非空且不以#号开头
            if data != '' and not data.startswith("#"):
                # 读取每行数据会将换行转换为\n,去掉每行数据中的\n
                self.caseList.append(data.replace("\n", ""))
        fb.close()

    def set_case_suite(self):
        # 通过set_case_list()拿到caselist元素组
        self.set_case_list()
        test_suite = unittest.TestSuite()
        suite_module = []
        # 从caseList元素组中循环取出case
        for case in self.caseList:
            # 通过split函数来将aaa/bbb分割字符串,-1取后面,0取前面
            case_name = case.split("/")[-1]
            print(case_name + ".py")
            # 批量加载用例,第一个参数为用例存放路径,第二个参数为路径文件名
            discover = unittest.defaultTestLoader.discover(start_dir=self.caseFile, pattern='test*.py',
                                                           top_level_dir=None)
            # 将discover存入suite_module元素组
            suite_module.append(discover)
            print('suite_module:' + str(suite_module))
        # 判断suite_module元素组是否存在元素
        if len(suite_module) > 0:
            # 如果存在,循环取出元素组内容,命名为suite
            for suite in suite_module:
                # 从discover中取出test_name,使用addTest添加到测试集
                for test_name in suite:
                    test_suite.addTest(test_name)
        else:
            print("else:")
            return None
        return test_suite

    def run(self):
        try:
            suit = self.set_case_suite()
            print("try")
            print(str(suit))
            if suit is not None:
                print("if-suit")
                fp = open(resultPath, 'wb')
                # 调用HTMLTestRunner
                runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title='Test Report',
                                                       description='Test Description')
                runner.run(suit)
            else:
                print("Have no case to test.")
        except Exception as ex:
            print(str(ex))
            # log.info(str(ex))
        finally:
            print("*********TEST END*********")
            # log.info("*********TEST END*********")
            fp.close()
        # 判断邮件发送的开关
        if on_off == 'on':
            send_mail.send_email()
        else:
            print("邮件发送开关配置关闭,请打开开关后可正常自动发送测试报告")


if __name__ == '__main__':
    AllTest().run()

执行 runAll.py,进到邮箱中查看发送的测试结果报告,打开查看
图片8.jpg
到这一步框架到基本搭建好了

日志记录

缺少日志的输出,在一些关键的参数调用的地方我们来输出一些日志。从而更方便的来维护和查找问题
在 common 下创建 Log.py,并写入:

import os
import logging
from logging.handlers import TimedRotatingFileHandler
import getpathInfo


path = getpathInfo.get_Path()
# 存放log文件的路径
log_path = os.path.join(path, 'result')


class Logger(object):
    def __init__(self, logger_name='logs…'):
        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()

然后我们在需要我们输出日志的地方添加日志。修改 runAll.py 文件,在顶部增加 import common.Log,然后增加标红框的代码
图片9.jpg
再来运行一下 runAll.py 文件,发现在 result 下多了一个 logs 文件
至此,我们的框架就搭建完成啦!

源码下载

源码地址:https://github.com/yongwei19/xmyInterfaceTest/