赞
踩
目录
测试确实是很简单,但是思路很重要。要做一个项目的UI自动化或者接口测试的自动化,都需要做一个规划,从整体出发构建。(类似很多公司的测试平台)
希望通过对本框架的练习,对后续编写测试框架起到抛砖引玉的效果。
首先配置好python环境和下载安装pycharm,在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/case:
——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:测试数据库连接池的文件,本次项目未用到数据库,可以忽略
想搭建一个接口自动化测试框架,前提是必须要有一个可支持测试的接口服务。可能有人想直接用公司的项目的现成接口,比如测试环境的或者是生产环境的接口。但是,一般工作环境中的接口,不太满足我们框架的各种条件。比如:接口a可能是get接口b可能又是post等等。因此我们自己编写一个简单的接口,用于我们这个框架的测试!
按照第一讲得目录创建好文件,打开test_api.py。加入以下代码:
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 11:33
- # @Author : Liqiju
- # @File : test_api.py
- # @Software : PyCharm
- import flask
- import json
- from flask import request
- import getpathInfo
- '''
- flask: web框架,通过flask提供的装饰器@server.route()将普通函数转换为服务
- '''
- # 创建一个服务,把当前这个python文件当做一个服务
- server = flask.Flask(__name__)
- #admin = Blueprint('getpathInfo',__name__)
- # @server.route()可以将普通函数转变为服务 登录接口的路径、请求方式
- @server.route('/login', methods=['get', 'post'])
- def login():
- # 获取通过url请求传参的数据
- username = request.values.get('name')
- # 获取url请求传的密码,明文
- pwd = request.values.get('pwd')
- # 判断用户名、密码都不为空
- if username and pwd:
- if username == 'xiaoming' and pwd == '111':
- resu = {'code': 200, 'message': '登录成功'}
- return json.dumps(resu, ensure_ascii=False) # 将字典转换字符串
- else:
- resu = {'code': -1, 'message': '账号或密码错误'}
- return json.dumps(resu, ensure_ascii=False)
- else:
- resu = {'code': 10001, 'message': '参数不能为空!'}
- return json.dumps(resu, ensure_ascii=False)
-
- @server.route('/getpath',methods=['get'])
- def getpath():
-
- return getpathInfo.get_Path()
-
- if __name__ == '__main__':
- server.run(debug=True, port=8888, host='127.0.0.1')
- # server.register_blueprint(admin,url_prefix='/getpath')
其中,以下2行代码的意思是把我们的接口(方法)提供出去供外界访问,route第一个参数为请求路径,第二个参数为请求方式。
- server = flask.Flask(__name__)
- @server.route('/login', methods=['get', 'post'])
执行test_api.py,在浏览器中输入http://127.0.0.1:8888/login?name=xiaoming&pwd=11188回车,验证我们的接口服务是否正常
变更参数,查看不同的响应结果确认接口服务一切正常
大家注意一下控制台输出,每一次成功的请求都会打印信息
在我们第二讲中,我们已经通过flask这个web框架创建好了我们用于测试的接口服务,因此我们可以把这个接口抽出来一些参数放到配置文件,然后通过一个读取配置文件的方法,方便后续的使用。同样还有邮件的相关配置
按第一讲的目录创建好config.ini文件,加入以下代码:
- # -*- coding: utf-8 -*-
- [HTTP]
- #协议
- scheme = http
- #URL
- baseurl = 127.0.0.1
- #端口
- port = 8888
- #超时时间
- timeout = 10.0
-
- [EMAIL]
- #邮件开关
- on_off = on
- #固定的SMTP服务器
- smtp_server = smtp.qq.com
- #固定端口
- smtp_port = 465
- #邮件主题
- subject = Python自动化接口测试报告
- #邮件发送账号
- from_addr = XXXX@qq.com
- #接收邮件账号,多个账号用逗号分隔
- to_addrs = XXX@yelinked.com,XXXXX@qq.com
- #授权码(这个要填自己获取到的,和from_addr对应)
- authorization_code = XXXXX
-
在HTTP中,协议http,baseURL,端口,超时时间
在EMAIL中,on-off是邮件的开关,=on的时候发送测试报告,=其他,则不发送。smtp_server为SMTP服务器,smtp_port为固定端口,subject为邮件主题,from_addr为邮件发送者账号,to_addrs为接收邮件账号(多个账号用逗号分隔)。
authorization_code为邮件发送者账号的授权码
在我们编写readConfig.py文件前,我们先写一个获取项目某路径下某文件绝对路径的一个方法。按第一讲的目录结构创建好getpathInfo.py,打开该文件,加入以下代码:
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 0024 11:32
- # @Author : Liqiju
- # @File : getpathInfo.py
- # @Software : PyCharm
- import os
- #from test_api import admin
-
- #@admin.route('/getpath',methods=['get'])
- def get_Path():
- path = os.path.split(os.path.realpath(__file__))[0]
- return path
-
-
- if __name__ == '__main__': # 执行该文件,测试下是否OK
- print('测试路径是否OK,路径为:', get_Path())
-
执行代码,查看输出结果,打印出了该项目的绝对路径:
接下来我们打开readConfig.py文件,加入以下代码
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 14:14
- # @Author : Liqiju
- # @File : readConfig.py
- # @Software : PyCharm
- import os
- import configparser
- import getpathInfo # 引入我们自己的写的获取路径的类
-
- path = getpathInfo.get_Path() # 调用实例化,还记得这个类返回的路径为F:\2020年11月\ifp\testFile
- config_path = os.path.join(path,'config.ini') # 这句话是在path路径下再加一级,最后变成F:\2020年11月\ifp\testFile\config.ini
- config = configparser.ConfigParser() # 调用外部的读取配置文件的方法
- config.read(config_path, encoding='utf-8')
-
-
- class ReadConfig():
-
- def get_http(self, name):
- value = config.get('HTTP', name)
- return value
-
- def get_email(self, name):
- value = config.get('EMAIL', name)
- return value
-
- def get_mysql(self, name): # 写好,留以后备用。但是因为我们没有对数据库的操作,所以这个可以屏蔽掉
- value = 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'))
执行代码,查看输出结果
一切OK
配置文件写好了,接口我们也有了,然后我们来根据我们的接口设计我们简单的几条用例。首先在前两讲中我们写了一个我们测试的接口服务,针对这个接口服务存在三种情况的校验。正确的用户名和密码、账号密码错误和账号或密码为空
我们根据上面的三种情况,将对这个接口的用例写在一个对应的单独文件中testFile\case\userCase.xlsx ,userCase.xlsx内容如下:
*注意sheet1改为login。
有了用例设计的Excel,我们要对这个Excel进行数据的读取操作,在readExcel.py文件,加入以下代码:
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 11:33
- # @Author : Liqiju
- # @File : readExecl.py
- # @Software : PyCharm
- import os
- import getpathInfo# 自己定义的内部类,该类返回项目的绝对路径
- from xlrd import open_workbook#调用读Excel的第三方库xlrd
- path = getpathInfo.get_Path() # 拿到该项目所在的绝对路径
-
- class readExecl():
- def get_xls(self,xls_name,sheet_name):
- """
- :param xls_name: 用例的Excel名称
- :param sheet_name: Excel的sheet名称
- :return:内容列表
- """
- cls = []
- # 获取用例文件路径
- xlsPath = os.path.join(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] != 'case_name': # 如果这个Excel的这个sheet的第i行的第一列不等于case_name那么我们把这行的数据添加到cls[]
- cls.append(sheet.row_values(rowx=i,start_colx=0,end_colx=None)) # 返回给定行中单元格值的一部分(这里返回每一行的内容)
- return cls
-
-
-
- if __name__ == '__main__':#我们执行该文件测试一下是否可以正确获取Excel中的值
- print(readExecl().get_xls('userCase.xlsx', 'login'))
- print(readExecl().get_xls('userCase.xlsx', 'login')[0][1])
- print(readExecl().get_xls('userCase.xlsx', 'login')[1][2])
-
执行代码,查看输出结果
结果正确
配置文件有了,读取配置文件有了,用例有了,读取用例有了,我们的接口服务有了,我们是不是该写对某个接口进行http请求了,这时候我们需要使用pip install requests来安装第三方库,在common下的configHttp.py加入以下代码:
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 11:28
- # @Author : Liqiju
- # @File : configHttp.py
- # @Software : PyCharm
- import requests
- import json
-
- class RunMain():
- def send_post(self,url,data): # 定义一个方法,传入需要的参数url和data
- # 参数必须按照url、data顺序传入
- result = requests.post(url=url,data=data).json()# 因为这里要封装post方法,所以这里的url和data值不能写死
- res = json.dumps(result,ensure_ascii=False,sort_keys=True,indent=2)
- return res
-
- def send_get(self, url, data):
- result = requests.get(url=url, params=data).json()
- res = json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2)
- return res
-
- def run_main(self, method, url=None, data=None): # 定义一个run_main函数,通过传过来的method来进行不同的get或post请求
- result = None
- if method == 'post':
- result = self.send_post(url, data)
- elif method == 'get':
- result = self.send_get(url,data)
- else:
- print("method值不是post或者get!!!")
- return result
-
- if __name__ == '__main__': # 通过写死参数,来验证我们写的请求是否正确(新手请注意保持test_api.py在运行,也就是服务器要打开)
- result1 = RunMain().run_main('post', 'http://127.0.0.1:8888/login', {'name': 'xiaoming','pwd':'111'})
- result2 = RunMain().run_main('get', 'http://127.0.0.1:8888/login', 'name=xiaoming&pwd=111')
- print(result1)
- print(result2)
执行以上代码,查看结果。
*注意要保持test_api.py在运行,不然报错目标主机拒绝连接。
在上一讲中,我们写了针对我们的接口服务,设计的三种测试用例,使用写死的参数(result = RunMain().run_main('post', 'http://127.0.0.1:8888/login', 'name=xiaoming&pwd='))来进行requests请求。本讲中我们写一个类,来用于分别获取这些参数,来第一讲的目录创建geturlParams.py,geturlParams.py文件中的内容如下:
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 11:32
- # @Author : Liqiju
- # @File : geturlParams.py
- # @Software : PyCharm
- import readConfig
-
- readconfig = readConfig.ReadConfig()
-
- class geturlParams(): # 定义一个方法,将从配置文件中读取的进行拼接
- def get_Url(self):
- new_url = readconfig.get_http('scheme') + '://' + readconfig.get_http('baseurl') + ':8888' + '/login' + '?'
- # logger.info('new_url'+new_url)
- return new_url
-
- if __name__ == '__main__': # 验证拼接后的正确性
- print(geturlParams().get_Url())
执行以上代码,查看结果。
通过将配置文件中的进行拼接,拼接后的结果:http://127.0.0.1:8888/login?和我们请求的一致
下面就可以用unittest断言测试case了,在testCase下创建test01case.py文件,加入以下代码:
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 0024 16:23
- # @Author : Liqiju
- # @File : test01case.py
- # @Software : PyCharm
- import json
- import unittest
- from common.configHttp import RunMain
- import paramunittest
- import geturlParams
- import urllib.parse
- import readExecl
-
- url = geturlParams.geturlParams().get_Url() # 调用我们的geturlParams获取我们拼接的URL
- login_xls = readExecl.readExecl().get_xls('userCase.xlsx', 'login')
-
- @paramunittest.parametrized(*login_xls)
- class testUserLogin(unittest.TestCase):
- def setParameters(self, case_name, path, query, method):
- """
- set params
- :param case_name:
- :param path
- :param query
- :param method
- :return:
- """
- self.case_name = str(case_name)
- self.path = str(path)
- self.query = str(query)
- self.method = str(method)
-
- def description(self):self.case_name
-
- def setUp(self):print(self.case_name + "测试开始前准备")
-
- def test01case(self):self.checkResult()
-
- def tearDown(self):print("测试结束,输出log完结\n\n")
-
- def checkResult(self): # 断言
- """
- check test result
- :return:
- """
- url1 = "http://www.xxx.com/login?"
- new_url = url1 + self.query
- data1 = dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(new_url).query)) # 将一个完整的URL中的name=&pwd=转换为{'name':'xxx','pwd':'bbb'}
- info = RunMain().run_main(self.method, url, data1) # 根据Excel中的method调用run_main来进行requests请求,并拿到响应
- ss = json.loads(info) # 将响应转换为字典格式
- if self.case_name == 'login': # 如果case_name是login,说明合法,返回的code应该为200
- self.assertEqual(ss['code'], 200)
- if self.case_name == 'login_error': # 同上
- self.assertEqual(ss['code'], -1)
- if self.case_name == 'login_null': # 同上
- self.assertEqual(ss['code'], 10001)
把鼠标放在下面注解的前面。执行
@paramunittest.parametrized(*login_xls)
查看结果
nice,继续下一步。
打开HTMLTestRunner.py文件,加入以下代码。
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 11:29
- # @Author : Liqiju
- # @File : HTMLTestRunner.py
- # @Software : PyCharm
- # -*- coding: utf-8 -*-
- """
- A TestRunner for use with the Python unit testing framework. It
- generates a HTML report to show the result at a glance.
- The simplest way to use this is to invoke its main method. E.g.
- import unittest
- import HTMLTestRunner
- ... define your tests ...
- if __name__ == '__main__':
- HTMLTestRunner.main()
- For more customization options, instantiates a HTMLTestRunner object.
- HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
- # output to a file
- fp = file('my_report.html', 'wb')
- runner = HTMLTestRunner.HTMLTestRunner(
- stream=fp,
- title='My unit test',
- description='This demonstrates the report output by HTMLTestRunner.'
- )
- # 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
- 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__ = "Li Qi Ju"
- __version__ = "0.9.1"
-
- """
- Change History
- Version 0.9.1
- * 用Echarts添加执行情况统计图 (灰蓝)
- Version 0.9.0
- * 改成Python 3.x (灰蓝)
- Version 0.8.3
- * 使用 Bootstrap稍加美化 (灰蓝)
- * 改为中文 (灰蓝)
- 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
- import sys
- import io
- import time
- import unittest
- from xml.sax import saxutils
-
-
- # ------------------------------------------------------------------------
- # 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: u'通过',
- 1: u'失败',
- 2: u'错误',
- }
-
- DEFAULT_TITLE = 'Unit Test Report'
- DEFAULT_DESCRIPTION = ''
-
- # ------------------------------------------------------------------------
- # 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://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet">
- <script src="https://cdn.bootcss.com/echarts/3.8.5/echarts.common.min.js"></script>
- <!-- <script type="text/javascript" src="js/echarts.common.min.js"></script> -->
- %(stylesheet)s
- </head>
- <body>
- <script language="javascript" type="text/javascript"><!--
- output_list = Array();
- /* level - 0:Summary; 1:Failed; 2:All */
- 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 < 1) {
- tr.className = 'hiddenRow';
- }
- else {
- tr.className = '';
- }
- }
- if (id.substr(0,2) == 'pt') {
- if (level > 1) {
- tr.className = '';
- }
- else {
- tr.className = 'hiddenRow';
- }
- }
- }
- }
- 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);
- }
- id_list[i] = tid;
- if (tr.className) {
- toHide = 0;
- }
- }
- for (var i = 0; i < count; i++) {
- tid = id_list[i];
- if (toHide) {
- document.getElementById('div_'+tid).style.display = 'none'
- document.getElementById(tid).className = 'hiddenRow';
- }
- else {
- document.getElementById(tid).className = '';
- }
- }
- }
- function showTestDetail(div_id){
- var details_div = document.getElementById(div_id)
- var displayState = details_div.style.display
- // alert(displayState)
- if (displayState != 'block' ) {
- displayState = 'block'
- details_div.style.display = 'block'
- }
- else {
- details_div.style.display = 'none'
- }
- }
- function html_escape(s) {
- s = s.replace(/&/g,'&');
- s = s.replace(/</g,'<');
- s = s.replace(/>/g,'>');
- return s;
- }
- /* obsoleted by detail in <div>
- function showOutput(id, name) {
- var w = window.open("", //url
- name,
- "resizable,scrollbars,status,width=800,height=450");
- d = w.document;
- d.write("<pre>");
- d.write(html_escape(output_list[id]));
- d.write("\n");
- d.write("<a href='javascript:window.close()'>close</a>\n");
- d.write("</pre>\n");
- d.close();
- }
- */
- --></script>
- <div id="div_base">
- %(heading)s
- %(report)s
- %(ending)s
- %(chart_script)s
- </div>
- </body>
- </html>
- """ # variables: (title, generator, stylesheet, heading, report, ending, chart_script)
-
- ECHARTS_SCRIPT = """
- <script type="text/javascript">
- // 基于准备好的dom,初始化echarts实例
- var myChart = echarts.init(document.getElementById('chart'));
- // 指定图表的配置项和数据
- var option = {
- title : {
- text: '测试执行情况',
- x:'center'
- },
- tooltip : {
- trigger: 'item',
- formatter: "{a} <br/>{b} : {c} ({d}%%)"
- },
- color: ['#95b75d', 'grey', '#b64645'],
- legend: {
- orient: 'vertical',
- left: 'left',
- data: ['通过','失败','错误']
- },
- series : [
- {
- name: '测试执行情况',
- type: 'pie',
- radius : '60%%',
- center: ['50%%', '60%%'],
- data:[
- {value:%(Pass)s, name:'通过'},
- {value:%(fail)s, name:'失败'},
- {value:%(error)s, name:'错误'}
- ],
- itemStyle: {
- emphasis: {
- shadowBlur: 10,
- shadowOffsetX: 0,
- shadowColor: 'rgba(0, 0, 0, 0.5)'
- }
- }
- }
- ]
- };
- // 使用刚指定的配置项和数据显示图表。
- myChart.setOption(option);
- </script>
- """ # variables: (Pass, fail, error)
-
- # ------------------------------------------------------------------------
- # 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,Consolas,arial,sans-serif; font-size: 80%; }
- table { font-size: 100%; }
- pre { white-space: pre-wrap;word-wrap: break-word; }
- /* -- heading ---------------------------------------------------------------------- */
- h1 {
- font-size: 16pt;
- color: gray;
- }
- .heading {
- margin-top: 0ex;
- margin-bottom: 1ex;
- }
- .heading .attribute {
- margin-top: 1ex;
- margin-bottom: 0;
- }
- .heading .description {
- margin-top: 2ex;
- margin-bottom: 3ex;
- }
- /* -- css div popup ------------------------------------------------------------------------ */
- a.popup_link {
- }
- a.popup_link:hover {
- color: red;
- }
- .popup_window {
- display: none;
- position: relative;
- left: 0px;
- top: 0px;
- /*border: solid #627173 1px; */
- padding: 10px;
- /*background-color: #E6E6D6; */
- font-family: "Lucida Console", "Courier New", Courier, monospace;
- text-align: left;
- font-size: 8pt;
- /* width: 500px;*/
- }
- }
- /* -- report ------------------------------------------------------------------------ */
- #show_detail_line {
- margin-top: 3ex;
- margin-bottom: 1ex;
- }
- #result_table {
- width: 99%;
- }
- #header_row {
- font-weight: bold;
- color: #303641;
- background-color: #ebebeb;
- }
- #total_row { font-weight: bold; }
- .passClass { background-color: #bdedbc; }
- .failClass { background-color: #ffefa4; }
- .errorClass { background-color: #ffc9c9; }
- .passCase { color: #6c6; }
- .failCase { color: #FF6600; font-weight: bold; }
- .errorCase { color: #c00; font-weight: bold; }
- .hiddenRow { display: none; }
- .testcase { margin-left: 2em; }
- /* -- ending ---------------------------------------------------------------------- */
- #ending {
- }
- #div_base {
- position:absolute;
- top:0%;
- left:5%;
- right:5%;
- width: auto;
- height: auto;
- margin: -15px 0 0 0;
- }
- </style>
- """
-
- # ------------------------------------------------------------------------
- # Heading
- #
-
- HEADING_TMPL = """
- <div class='page-header'>
- <h1>%(title)s</h1>
- %(parameters)s
- </div>
- <div style="float: left;width:50%%;"><p class='description'>%(description)s</p></div>
- <div id="chart" style="width:50%%;height:400px;float:left;"></div>
- """ # variables: (title, parameters, description)
-
- HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
- """ # variables: (name, value)
-
- # ------------------------------------------------------------------------
- # Report
- #
-
- REPORT_TMPL = u"""
- <div class="btn-group btn-group-sm">
- <button class="btn btn-default" onclick='javascript:showCase(0)'>总结</button>
- <button class="btn btn-default" onclick='javascript:showCase(1)'>失败</button>
- <button class="btn btn-default" onclick='javascript:showCase(2)'>全部</button>
- </div>
- <p></p>
- <table id='result_table' class="table table-bordered">
- <colgroup>
- <col align='left' />
- <col align='right' />
- <col align='right' />
- <col align='right' />
- <col align='right' />
- <col align='right' />
- </colgroup>
- <tr id='header_row'>
- <td>测试套件/测试用例</td>
- <td>总数</td>
- <td>通过</td>
- <td>失败</td>
- <td>错误</td>
- <td>查看</td>
- </tr>
- %(test_list)s
- <tr id='total_row'>
- <td>总计</td>
- <td>%(count)s</td>
- <td>%(Pass)s</td>
- <td>%(fail)s</td>
- <td>%(error)s</td>
- <td> </td>
- </tr>
- </table>
- """ # variables: (test_list, count, Pass, fail, error)
-
- REPORT_CLASS_TMPL = u"""
- <tr class='%(style)s'>
- <td>%(desc)s</td>
- <td>%(count)s</td>
- <td>%(Pass)s</td>
- <td>%(fail)s</td>
- <td>%(error)s</td>
- <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">详情</a></td>
- </tr>
- """ # variables: (style, desc, count, Pass, fail, error, cid)
-
- 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'>
- <!--css div popup start-->
- <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
- %(status)s</a>
- <div id='div_%(tid)s' class="popup_window">
- <pre>%(script)s</pre>
- </div>
- <!--css div popup end-->
- </td>
- </tr>
- """ # variables: (tid, Class, style, desc, status)
-
- 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'>%(status)s</td>
- </tr>
- """ # variables: (tid, Class, style, desc, status)
-
- REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s""" # variables: (id, output)
-
- # ------------------------------------------------------------------------
- # ENDING
- #
-
- ENDING_TMPL = """<div id='ending'> </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
-
- # result is a list of result in 4 tuple
- # (
- # result code (0: success; 1: fail; 2: error),
- # TestCase object,
- # Test output (byte string),
- # stack trace,
- # )
- self.result = []
- self.subtestlist = []
-
- def startTest(self, test):
- TestResult.startTest(self, test)
- # just one buffer for both stdout and stderr
- self.outputBuffer = io.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):
- if test not in self.subtestlist:
- 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('\n')
- 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('\n')
- 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('\n')
- else:
- sys.stderr.write('F')
-
- def addSubTest(self, test, subtest, err):
- if err is not None:
- if getattr(self, 'failfast', False):
- self.stop()
- if issubclass(err[0], test.failureException):
- self.failure_count += 1
- errors = self.failures
- errors.append((subtest, self._exc_info_to_string(err, subtest)))
- output = self.complete_output()
- self.result.append((1, test, output + '\nSubTestCase Failed:\n' + str(subtest),
- self._exc_info_to_string(err, subtest)))
- if self.verbosity > 1:
- sys.stderr.write('F ')
- sys.stderr.write(str(subtest))
- sys.stderr.write('\n')
- else:
- sys.stderr.write('F')
- else:
- self.error_count += 1
- errors = self.errors
- errors.append((subtest, self._exc_info_to_string(err, subtest)))
- output = self.complete_output()
- self.result.append(
- (2, test, output + '\nSubTestCase Error:\n' + str(subtest), self._exc_info_to_string(err, subtest)))
- if self.verbosity > 1:
- sys.stderr.write('E ')
- sys.stderr.write(str(subtest))
- sys.stderr.write('\n')
- else:
- sys.stderr.write('E')
- self._mirrorOutput = True
- else:
- self.subtestlist.append(subtest)
- self.subtestlist.append(test)
- self.success_count += 1
- output = self.complete_output()
- self.result.append((0, test, output + '\nSubTestCase Pass:\n' + str(subtest), ''))
- if self.verbosity > 1:
- sys.stderr.write('ok ')
- sys.stderr.write(str(subtest))
- sys.stderr.write('\n')
- else:
- sys.stderr.write('.')
-
-
- class HTMLTestRunner(Template_mixin):
-
- def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=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
-
- 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('\nTime Elapsed: %s' % (self.stopTime - self.startTime), file=sys.stderr)
- 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 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
-
- 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 = []
- if result.success_count: status.append(u'通过 %s' % result.success_count)
- if result.failure_count: status.append(u'失败 %s' % result.failure_count)
- if result.error_count: status.append(u'错误 %s' % result.error_count)
- if status:
- status = ' '.join(status)
- else:
- status = 'none'
- return [
- (u'开始时间', startTime),
- (u'运行时长', duration),
- (u'状态', status),
- ]
-
- def generateReport(self, test, result):
- report_attrs = self.getReportAttributes(result)
- generator = 'HTMLTestRunner %s' % __version__
- stylesheet = self._generate_stylesheet()
- heading = self._generate_heading(report_attrs)
- report = self._generate_report(result)
- ending = self._generate_ending()
- chart = self._generate_chart(result)
- output = self.HTML_TMPL % dict(
- title=saxutils.escape(self.title),
- generator=generator,
- stylesheet=stylesheet,
- heading=heading,
- report=report,
- ending=ending,
- chart_script=chart
- )
- self.stream.write(output.encode('utf8'))
-
- def _generate_stylesheet(self):
- return self.STYLESHEET_TMPL
-
- 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),
- )
- return heading
-
- 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("\n")[0] or ""
- desc = doc and '%s: %s' % (name, doc) or name
-
- row = self.REPORT_CLASS_TMPL % dict(
- style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
- 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),
- )
- return report
-
- def _generate_chart(self, result):
- chart = self.ECHARTS_SCRIPT % dict(
- Pass=str(result.success_count),
- fail=str(result.failure_count),
- error=str(result.error_count),
- )
- return chart
-
- 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)
- tid = (n == 0 and 'p' or 'f') + '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
-
- script = self.REPORT_TEST_OUTPUT_TMPL % dict(
- id=tid,
- output=saxutils.escape(o + e),
- )
-
- row = tmpl % dict(
- tid=tid,
- Class=(n == 0 and 'hiddenRow' or 'none'),
- style=(n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none')),
- 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 HTMLTestRunner as the default test runner.
- # base class's testRunner parameter is not useful because it means
- # we have to instantiate HTMLTestRunner before we know self.verbosity.
- if self.testRunner is None:
- self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
- unittest.TestProgram.runTests(self)
-
-
- main = TestProgram
-
- ##############################################################################
- # Executing this module from the command line
- ##############################################################################
-
- if __name__ == "__main__":
- main(module=None)
解释:HTMLTestRunner是测试报告的源码。因为源代码是英文的,而且少了一个测试人员这个参数,一般都会复制出来单独改动。就像ddt里面要去读取casename这个参数一样,也会去改源码。
按照目录结构,打开caselist.txt文件,加入下面的内容
- user/test01case
- #user/test02case
- #user/test03case
- #user/test04case
- #user/test05case
- #alarm/test_alarm_list
- #alarm/test_alarm_detail
这个文件的作用是,我们通过这个文件来控制,执行哪些模块下的哪些unittest用例文件。如在实际的项目中:user模块下的test01case.py,设备告警alarm模块下的告警列表test_alarm_list,如果本轮无需执行某些模块的用例的话,就在前面添加#。我们继续,还缺少一个发送邮件的文件。打开common下的configEmail.py文件,加入以下代码:
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 11:28
- # @Author : Liqiju
- # @File : configEmail.py
- # @Software : PyCharm
- import os
- import smtplib
- from email.mime.text import MIMEText
- from email.header import Header
- from email.mime.multipart import MIMEMultipart
- from email.utils import formataddr
- import readConfig
- import getpathInfo
-
- read_conf = readConfig.ReadConfig() #配置类实例
- on_off = read_conf.get_email('on_off') #配置文件读取邮件开关
- smtp_server = read_conf.get_email('smtp_server')#配置文件读取SMTP服务器
- smtp_port = read_conf.get_email('smtp_port')#配置文件读取SMTP服务器端口
- subject = read_conf.get_email('subject')#配置文件读取邮件主题
- from_addr = read_conf.get_email('from_addr')#配置文件读取发件人
- to_addrs = read_conf.get_email('to_addrs')#配置文件读取收件人
- authorization_code = read_conf.get_email('authorization_code')#配置文件读取授权码
- report_path = os.path.join(getpathInfo.get_Path(),'result','report.html')#获取测试报告路径
-
- class Send_email():
- def __init__(self,on_off,smtp_server,smtp_port,subject,from_addr,to_addrs,authorization_code,report_path):
- self.on_off = on_off
- self.smtp_server = smtp_server
- self.smtp_port = smtp_port
- self.subject = subject
- self.from_addr = from_addr
- self.to_addrs = to_addrs
- self.authorization_code = authorization_code
- self.report_path = report_path
-
- def qq_email(self):
- # 配置服务器
- stmp = smtplib.SMTP_SSL(smtp_server, smtp_port)
- stmp.login(from_addr, authorization_code)
-
- # 创建一个带附件的实例
- message = MIMEMultipart()
- message['From'] = formataddr(["李自动化测试", from_addr]) # 发件人
- message['To'] = to_addrs # 收件人,如果to_addrs直接用列表这里就会报错
- message['Subject'] = Header(subject, 'utf-8') # 邮件标题
- message.attach(MIMEText('渊联测试云平台自动发送,\n附件report.html为测试报告,请查阅', 'plain', 'utf-8')) # 邮件正文内容
-
- # 增加HTML附件
- atthtml = MIMEText(open(report_path, 'rb').read(), 'base64','utf-8') # 文件放在同一路径,不放在同一路径改一下比如'D:\\test\\report.html
- atthtml["Content-Type"] = 'application/octet-stream'
- atthtml["Content-Disposition"] = 'attachment;filename = "report.html"'
- message.attach(atthtml)
- try:
- if on_off == 'on':
- stmp.sendmail(from_addr, to_addrs.split(','), message.as_string()) # 发送邮件,split分割后产生列表
- print('邮件发送成功')
- else:
- print("邮箱开关未打开")
- except Exception as e:
- print('邮件发送失败--' + str(e))
-
- # 运行此文件来验证写的qq_email是否正确
- if __name__ == '__main__':
- print(subject)
- send_email = Send_email(on_off,smtp_server,smtp_port,subject,from_addr,to_addrs,authorization_code,report_path)
- send_email.qq_email()
执行以上程序,查看结果。
在邮箱中查看
完全OK。这一块有不懂的参考我的另外一篇博客。 python SMTP实现QQ邮箱发送附件带HTML的邮件(规范发件人和收件人地址)
打开runAll.py文件,加入以下代码:
- # -*- coding: utf-8 -*-
- # @Time : 2020/12/1 11:33
- # @Author : Liqiju
- # @File : runAll.py
- # @Software : PyCharm
- import os
- import common.HTMLTestRunner as HTMLTestRunner
- import getpathInfo
- import unittest
- import readConfig
- from common.configEmail import Send_email
- #引入定时框架APScheduler
- from apscheduler.schedulers.blocking import BlockingScheduler
-
- import common.Log
-
- read_conf = readConfig.ReadConfig() #配置类实例
- on_off = read_conf.get_email('on_off') #配置文件读取邮件开关
- smtp_server = read_conf.get_email('smtp_server')#配置文件读取SMTP服务器
- smtp_port = read_conf.get_email('smtp_port')#配置文件读取SMTP服务器端口
- subject = read_conf.get_email('subject')#配置文件读取邮件主题
- from_addr = read_conf.get_email('from_addr')#配置文件读取发件人
- to_addrs = read_conf.get_email('to_addrs')#配置文件读取收件人
- authorization_code = read_conf.get_email('authorization_code')#配置文件读取授权码
- report_path = os.path.join(getpathInfo.get_Path(),'result','report.html')#获取测试报告路径
-
- 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
- resultPath = os.path.join(report_path,"report.html") #result/report.html
- self.caseListFile = os.path.join(path,"caselist.txt") #配置执行哪些测试文件的配置文件路径
- print("初始化配置文件路径",self.caseListFile)
- self.caseFile = os.path.join(path,"testCase") #真正的测试断言文件路径
- self.caseList = []
-
- log.info(resultPath) #将resultPath的值输入到日志,方便定位查看问题
- log.info(self.caseListFile) #同理
- log.info(self.caseList) #同理
-
- 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('\n','')) #读取每行数据会将换行转换为\n,去掉每行数据中的\
- fb.close()
-
- def set_case_suite(self):
- 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('执行set_case_suite的else部分代码')
- return None
- return test_suite # 返回测试集
-
- def run(self):
- """
- run test
- :return:
- """
- try:
- suit = self.set_case_suite() # 调用set_case_suite获取test_suite
- #print('执行try代码')
- print("--------------------",str(suit))
- if suit is not None: # 判断test_suite是否为空
- fp = open(resultPath, 'wb') # 打开result/report.html测试报告文件,如果不存在就创建
- # 调用HTMLTestRunner
- runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title='Test Report', description='Test Description')
- runner.run(suit)
- else:
- print("没有案例可以测试")
- except Exception as ex:
- print(str(ex))
- log.info(str(ex))
-
- finally:
- print("*********测试结束*********")
- log.info("*********测试结束*********")
- fp.close()
- # 判断邮件发送的开关
- if on_off == 'on':
- send_email = Send_email(on_off, smtp_server, smtp_port, subject, from_addr, to_addrs, authorization_code,report_path)
- send_email.qq_email()
- else:
- print("邮件发送开关配置关闭,打开开关后可正常发送测试报告")
- #scheduler = BlockingScheduler()
- #scheduler.add_job(ALLTest().run, 'interval', hours=1)
- #scheduler.add_job(ALLTest().run, 'interval', seconds=10)
- #scheduler.start()
-
- if __name__ == '__main__':
- ALLTest().run()
执行以上代码,查看结果。
去邮箱打开report.html查看
一切OK。 至此我们的框架基本搭建好了。但是缺少日志的输出,需要在一些关键的参数调用的地方我们来输出一些日志。从而更方便的来维护和查找问题。请看下一讲。
打开common下的Log.py文件,加入以下代码:
- # -*- coding: utf-8 -*-
- # @Time : 2020/11/24 0024 11:29
- # @Author : Liqiju
- # @File : Log.py.py
- # @Software : PyCharm
- import os
- import logging
- from logging.handlers import TimedRotatingFileHandler
- import getpathInfo
-
- path = getpathInfo.get_Path()
- log_path = os.path.join(path,'result') #存放log文件路径
-
-
- 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,然后增加标红框的代码
再次运行runALL.py。 打开logs文件
发现logs正确打印
有了日志,还不够。我们再添加一个定时任务。请看下一讲
这里我们利用的是定时框架Apscheduler。关于定时任务,网上很多用法,大家有兴趣可以后面自己研究。
更改runALL.py文件,把下面红线的3行注释,释放,再次运行。
我这里为了便于测试设置了10秒一次,发送得很频繁。大家根据实际的需要设置或者改为其他比如一天执行一次的用法。
我这里收件人有2个,一个企业邮箱,一个qq邮箱。
自己的企业邮箱
另外一个QQ邮箱
完全OK。至此,恭喜你,框架基本搭建好了。
后续我们可以对此框架进行进一步的优化改造,使用我们的真实项目接口,加入数据库的校验等等。
有疑问可以一起探讨!
写在文章最后,推荐几本测试人员必备的python好书。共勉(第一本了解接口测试,第二本扎实python基础,第三本熟悉掌握python流行的框架Django)
2020年12月7日
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。