接口测试 一个简单的接口测试框架 demo

Nobita · September 06, 2020 · Last by Nobita replied at September 07, 2020 · 1665 hits

Python接口自动化测试框架

基于 Requests+Unittest+HTMLTestRunner,用Excel管理测试用例.

正文

有时候也会问自己为什么要重复造轮子,开源框架一搜一堆。后来想想,可能我在乎的不是目的地,而是沿途的风景。

【框架流程图】

【Common部分】
常见的接口都是走http协议,对requests库进行post/get请求方法的封装。

# -*- 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): # 定义一个方法,传入需要的参数urlheadersdata
# 参数必须按照urlheadersdata顺序传入
headers = headers
result_data = requests.post(url=url, headers=headers, data=data).json() # 因为这里要封装post方法,所以这里的urldata值不能写死
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来进行不同的getpost请求
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模块进行封装。

# -*- 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:\n 附件为接口自动化测试报告,此为自动发送邮件,请勿回复,谢谢!', '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断言模块的封装。

# -*- 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日志模块的封装。

# -*- 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

对各种常见加密方法的封装。

# -*- 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

# -*- 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文件方法的封装。

# -*- 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_nameExcelsheet名称
cls = []
# 获取用例文件路径
xlsPath = os.path.join(self.path, "TestFile", 'case', xls_name)
file = open_workbook(xlsPath) # 打开用例Excel
sheet = file.sheet_by_name(sheet_name) # 获得打开Excelsheet
# 获取这个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; width: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('\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')


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, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
sys.stderr.write('\nTime 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)
# 合入Githubboafantasy代码
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("\n")[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 \n 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 \n 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方法的封装

# -*- 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部分】
flask开发的接口demo,具体代码参考github,这里不做详细介绍。

【Log部分】
文件夹logs用来存储log日志的文件
日志输出内容预览:

【框架流程图部分】
存放此接口框架的流程图,文件名:此框架流程图.xmind

【Report部分】
存放测试结束后生成的html接口测试报告,文件名:report.html

【TestCase部分】
用来存放各个接口的测试用例。这里我举两个接口栗子。
[ 栗子①:/login ]

# -*- 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完结\n\n")

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 ]

# -*- coding: utf-8 -*-
"""
@File:test_header.py
@E-mail:364942727@qq.com
@Time:2020/9/3 11:28 下午
@Author:Nobita
@Version:1.0
@Desciption:/header接口的测试用例及断言
"""


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完结\n\n")

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部分】
用来存放接口项目的测试数据,采用Excel方式管理,具体内容参考github上的文件内容。

【getpathInfo部分】
用来获取项目的文件路径,一般都放在工程根目录。

# -*- 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部分】
对项目所有功能模块调用的封装。

# -*- coding: utf-8 -*-
"""
@File:RunAll.py
@E-mail:364942727@qq.com
@Time:2020/9/6 17:58 下午
@Author:Nobita
@Version:1.0
@Desciption:项目总执行脚本
"""


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("\n", "")) # 读取每行数据会将换行转换为\n,去掉每行数据中的\n
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上的文件内容。

结束语

这个周末没有睡懒觉。。。整理了这个接口框架demo分享给入门的新人,

更多功能需要结合生产上的业务需求进行开发挖掘。

学习和工作是一个循序渐进,不断肯定以及不断否定自我的过程。

希望我们能在此过程中像代码一样迭代自我,加油!

如果方便的话,在github上给我个小星星,在这里提前跪谢大佬了,么么哒。

github源码下载地址:https://github.com/charseki/API_Auto_Test

共收到 6 条回复 时间 点赞

你好楼主,我想请问下 其中的测试平台是直接使用的django + xadmin后台一套写的嘛,还是说是单独的前端展示页面(及路由)并非用的xadmin本身的后台管理系统改造而来

柒意 回复

前后端分离的,Flask+Vue+Nginx+Gunicorn 部署的平台。

Nobita 回复

👌,谢谢回答,学习了

有点类似于UI自动化的PO模式写出来的框架。将各个功能分到了对应的文件里面,直接UI自动化跟这个思路差不多,接口也用这个思路写过

是的,把代码和测试数据尽可能的分离。让测试代码可读性更好,可维护性更强,复用性更高。

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up