目录
pytest推荐
- 简单灵活
- 支持参数化配置 -v -s -k 等
- 支持ini文件
- 支持conftest
- 与requests配合做单元测试,场景测试都很ok
allure推荐
- 可视化界面报告
- 接口耗时、响应时间、成功、失败
- 总之就是可以一眼看到测试用例的运行情况及报错信息,快速分析定位出用例问题
jenkins
- jenkins就不多讲了,主要用与持续化,定时运行,邮件发送等,总之非常强大
ok,我们开始
框架架构
config
config层主要是封装了一些邮件发送的功能及日志功能,本文章没有用到smt.py文件,邮件发送走jenkins发送
log.py
日志封装
import logging import time from logging.handlers import TimedRotatingFileHandler from global_path import * from methods.common import * def loger(level='DEBUG'): level = str(level).upper() #定义收集器 logger = logging.getLogger('test') #配置收集器等级 logger.setLevel(level) #配置控制台打印 stearm_log = logging.StreamHandler() stearm_log.setLevel(level) #配置日志收集器--以时间维度进行统计 filelog_name = fr'{log_path}\\{log_time()}.log' """ #实例化TimedRotatingFileHandler # filename:日志文件名 # when:日志文件按什么切分。'S'-秒;'M'-分钟;'H'-小时;'D'-天;'W'-周 # 这里需要注意,如果选择 D-天,那么这个不是严格意义上的'天',是从你 # 项目启动开始,过了24小时,才会重新创建一个新的日志文件,如果项目重启, # 这个时间就会重置。选择'MIDNIGHT'-是指过了凌晨12点,就会创建新的日志 # interval是时间间隔 # backupCount:是保留日志个数。默认的0是不会自动删除掉日志。如果超过这个个数,就会自动删除 """ file_log = TimedRotatingFileHandler(filelog_name, when='MIDNIGHT', interval=1, backupCount=3) file_log.setLevel(level) #打印格式 formatter = '[%(asctime)s] [%(threadName)s] [line:%(lineno)d] %(levelname)s: %(message)s' log_fmt = logging.Formatter(formatter) #配置打印格式 stearm_log.setFormatter(log_fmt) file_log.setFormatter(log_fmt) #配置打印器追加到容器 logger.addHandler(stearm_log) logger.addHandler(file_log) return logger if __name__ == '__main__': a = loger('info') a.info('测试日志')
smt.py
邮件模块
# smtplib 用于邮件的发信动作
import smtplib
# email 用于构建邮件内容
from email.mime.text import MIMEText
# 构建邮件头
from email.header import Header
# 发信方的信息:发信邮箱,QQ 邮箱授权码
from_addr = '1798830003@qq.com'
password = '*****'
# 收信方邮箱
to_addr = '1798830003@qq.com'
# 发信服务器
smtp_server = 'smtp.qq.com'
# 邮箱正文内容,第一个参数为内容,第二个参数为格式(plain 为纯文本),第三个参数为编码
html_msg = """
<p>Python 邮件发送HTML格式文件测试...</p>
<p><a href="生成allure报告的url即可">这是一个链接</a></p>
"""
msg = MIMEText(html_msg, 'html', 'utf-8')
# 邮件头信息
msg['From'] = Header('张三') # 发送者
msg['To'] = Header('李四') # 接收者
subject = 'Python SMTP 邮件测试'
msg['Subject'] = Header(subject, 'utf-8') # 邮件主题
try:
smtpobj = smtplib.SMTP_SSL(smtp_server)
# 建立连接--qq邮箱服务和端口号(可百度查询)
smtpobj.connect(smtp_server, 465)
# 登录--发送者账号和口令
smtpobj.login(from_addr, password)
# 发送邮件
smtpobj.sendmail(from_addr, to_addr, msg.as_string())
print("邮件发送成功")
except smtplib.SMTPException:
print("无法发送邮件")
finally:
# 关闭服务器
smtpobj.quit()
data层
主要是针对环境数据及基础数据维护的yaml文件,针对yaml文件变量读取,我在methods的get_yaml.py中封装了对应的方法
test为测试环境
uat为生产环境
data.yaml
environment: uat
test:
username: xxx
password: xxx
Authorization: xxx
loginurl: xxx
device: xxx
xxx: xxx
uat:
username: xxx
password: xxx
loginurl: xxx
device: xxx
xxx: xxx
methods层
主要封装一些公共方法,方便用例层,接口层调用
common.py
公共方法(当前时间、MD5加密、随机数)等,可封装在此
import time, hashlib, string, random
from jsonpath import jsonpath
def data_time():
'''
当前时间
:return:
'''
local_time = time.strftime('%Y-%m-%d %H:%M:%S')
return local_time
def time_code():
'''
当前时间
:return:
'''
local_time = time.strftime('%Y%m%d%H%M%S')
return local_time
def log_time():
'''
日志时间生成器
:return:
'''
log_time = time.strftime('%Y-%m-%d')
return log_time
def MD5_decode(pwd):
'''
解码
:param pwd: 密码
:return:
'''
m = hashlib.md5()
m.update(pwd.encode('utf-8'))
return m.hexdigest()
def random_int(count):
'''
生成数值、英文的随机数
:param count: 生成的长度
:return:
'''
int_num = string.digits
english_num = string.ascii_uppercase
num = random.sample(int_num + english_num, count)
# print(num)
znum = ''.join(num)
print(f'生成随机数的结果为>>: {znum}')
return znum
def list_dict(list1: list, list2: list):
'''
2列表转json
:param list1:
:param list2:
:return:
'''
dict_list = dict(zip(list1, list2))
return dict_list
def jsonpath_draw(dict_list: dict, key=None, twokey=None):
if key == None:
vlaue = jsonpath(dict_list, f'$.{twokey}')
return vlaue
else:
value = jsonpath(dict_list, f'$.{key}.{twokey}')
return value
get_yaml.py
根据不同的环境,获取yaml文件中的对应的数据
import yaml
from global_path import *
from config.log import *
def yaml_data():
with open(datayaml_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
if data['environment'] == 'test':
data = data['test']
elif data['environment'] == 'uat':
data = data['uat']
return data
http_client.py
基于request做了封装调用,并加上日志数据。方便调试与问题排查
import requests
import hashlib
import traceback
import json
from config.log import loger
class HttpClient(object):
def __init__(self):
self.session = requests.Session()
self.log = loger('info')
def request(self, requestMethod, requesturl, paramMethod=None, requestData=None, headers=None, **kwargs):
try:
if requestMethod.lower() == "post":
self.log.info(f'\n请求格式 {str(requestMethod)}\n' +
f'请求url:{str(requesturl)}\n' +
f'传参类型:{str(paramMethod)}\n' +
f'body: {str(requestData)}\n' +
f'头部: {str(headers)}\n')
return self.__post(requesturl=requesturl, paramMethod=paramMethod, requestData=requestData,
headers=headers, **kwargs)
elif requestMethod.lower() == "get":
self.log.info(f'\n请求格式 {str(requestMethod)}\n' +
f'请求url:{str(requesturl)}\n' +
f'传参类型:{str(paramMethod)}\n' +
f'body {str(requestData)}\n' +
f'头部 {str(headers)}\n')
return self.__get(requesturl, requestData=requestData, headers=headers, **kwargs)
elif requestMethod.lower() == "patch":
self.log.info(f'\n请求格式 {str(requestMethod)}\n' +
f'请求url:{str(requesturl)}\n' +
f'传参类型:{str(paramMethod)}\n' +
f'body {str(requestData)}\n' +
f'头部 {str(headers)}\n')
return self.__patch(requesturl, paramMethod, requestData=requestData, headers=headers, **kwargs)
except Exception as e:
print(traceback.format_exc())
def __post(self, requesturl, paramMethod, requestData=None, headers=None, **kwargs):
try:
if paramMethod == "form" or paramMethod == "data":
responseObj = self.session.post(url=requesturl, data=requestData, headers=headers, **kwargs)
self.log.info(f'--结果-- {str(responseObj.json())}')
return responseObj
elif paramMethod == 'json':
responseObj = self.session.post(url=requesturl, json=requestData, headers=headers, **kwargs)
self.log.info(f'--结果-- {str(responseObj.json())}')
return responseObj
elif paramMethod == 'files':
responseObj = self.session.post(url=requesturl, files=requestData, headers=headers, **kwargs)
self.log.info(f'--结果-- {str(responseObj.json())}')
return responseObj
except Exception as e:
print(traceback.format_exc())
def __get(self, requestUrl, requestData=None, headers=None, **kwargs):
try:
if requestData:
responseObj = self.session.get(url=requestUrl, data=requestData, headers=headers, **kwargs)
self.log.info(f'响应结果 {str(responseObj.status_code)}')
self.log.info(f'--结果-- {str(responseObj.json())}')
return responseObj
else:
responseObj = self.session.get(url=requestUrl, headers=headers, **kwargs)
self.log.info(f'响应结果 {str(responseObj.status_code)}')
self.log.info(f'--结果-- {str(responseObj.json())}')
return responseObj
except Exception as e:
print(traceback.format_exc())
def __patch(self, requesturl, paramMethod, requestData=None, headers=None, **kwargs):
try:
if paramMethod == "json" or paramMethod == "data":
responseObj = self.session.patch(url=requesturl, data=requestData, **kwargs)
self.log.info(f'响应结果 {str(responseObj.status_code)}')
self.log.info(f'--结果-- {str(responseObj.json())}')
return responseObj
except Exception as e:
print(traceback.format_exc())
report报告层
html
html生成的路径位置
xml
用例运行过程中生成的json数据存储的地方
request_list层
接口层,根据业务需要整理对应的接口,可以以业务模块进行分py,也可以根据场景进行分py
作者是根据系统模块层级分的py文件
TY-login.py
就拿本次项目的登录做讲解,登录py主要是获取系统中的token,拿到token后将token信息传递给其他接口使用
userdata=yaml_data() 获取get_yaml中yaml_data拿到的yaml信息
self.http = HttpClient() 封装接口方法调用
self.url = userdata['loginurl'] 环境的url 如:测试环境为“xxx.dataserver.cn” 生产环境:“xxxx.baidu.com”
from methods.http_client import HttpClient
from methods.common import MD5_decode
from config.log import loger
from methods.get_yaml import yaml_data
class TY_testlogin():
'''
登录 台一
'''
def __init__(self):
userdata = yaml_data()
self.http = HttpClient()
self.url = userdata['loginurl']
def token(self, userdata):
'''
获取token
:param userdata:
:return:
'''
url = self.url + 'certification/token'
data = {'username': userdata['username'],
'password': userdata['password'],
'grant_type': 'rsa_password'}
headers = {'Authorization': userdata['Authorization']}
res = self.http.request(requestMethod='post',
paramMethod='form',
requesturl=url,
requestData=data,
headers=headers)
# print(res.status_code)
assert 201 == res.status_code
TY_token = res.json()['data']['access_token']
return TY_token
TY-requestlist.py
基础数据业务接口,主要依赖login中获取的token数据。给业务接口使用
token = TY_testlogin().token(userdata)
self.header = {'Authorization': f'bearer {token}'}
# coding=utf-8
from methods.http_client import HttpClient
from methods.get_yaml import yaml_data
from request_list.TY_login import TY_testlogin
class Basic_data:
'''
基础数据
'''
def __init__(self):
self.http = HttpClient()
userdata = yaml_data()
self.url = userdata['loginurl']
token = TY_testlogin().token(userdata)
self.header = {'Authorization': f'bearer {token}'}
def finished_product(self, code, name, remarks=None):
"""
新增产成品类型
:param code: 编号
:param name: 名称
:param remarks: 备注
:return:
"""
url = self.url + '/instances'
data = {
"objectType": "ZYWL_BASIC_FINISHED_PRODUCT_TYPE",
"identifier": code,
"name": name,
"description": remarks
}
res = self.http.request(requestMethod='post',
paramMethod='json',
requesturl=url,
requestData=data,
headers=self.header)
print(type(res.status_code))
assert 200 == res.status_code
return res.json()
def search_product(self, code):
'''
搜索产成品类型
:param code: 编号
:return:
'''
url = self.url + /datalinks'
data = {
"linkIds": [
"1563456803885420544"
],
"params": {
"page": 1,
"pageSize": 10000,
"searchName": "",
"searchNumber": code
}
}
res = self.http.request(requestMethod='post',
paramMethod='json',
requesturl=url,
requestData=data,
headers=self.header)
assert 200 == res.status_code
return res.json()
def delete_product(self, id):
'''
删除产成品类型
:param id: id
:return:
'''
url = self.url + 'api/batch?profileIdentifier=PRODUCT_TYPE'
data = {
"data": [
id
],
"method": "delete"
}
res = self.http.request(requestMethod='post',
paramMethod='json',
requesturl=url,
requestData=data,
headers=self.header)
assert 200 == res.status_code
return res.json()
test_case层
用例层,用例场景的集合,使用alluer结合pytest编写
Basic_data
- 主要就是根据业务场景使用allure定义具体的步骤及用common中封装的一些通用方法,生成一些随机数,作为数据支撑完成测试场景的接口自动化
import pytest, allure
from global_path import *
from methods.common import time_code
from request_list.TY_login import TY_testlogin
from methods.get_yaml import yaml_data
from request_list.TY_requestlist import Basic_data
@allure.feature('基础数据')
class Test_old():
def setup_class(self):
print('-----用例开始------')
self.Basic_data = Basic_data()
@allure.story('产成品类型')
def test_add_finished_product(self):
allure.dynamic.title('新增')
with allure.step('新增'):
add = self.Basic_data.finished_product(code=f'auto{time_code()}', name='auto产成品', remarks=None)
result = add['message']
allure.attach(result, name='新增产成品结果', attachment_type=allure.attachment_type.TEXT)
assert result == '操作成功'
@allure.story('产成品类型')
def test_search_finished_product(self):
allure.dynamic.title('搜索')
with allure.step('搜索'):
search = self.Basic_data.search_product(code='auto')
result = search[0]['data'][-1][2]
allure.attach(result, name='搜索产成品结果', attachment_type=allure.attachment_type.TEXT)
assert 'auto' in result
global_path.py
- 项目路径层,定义每个包,在项目中的路径,方便代码调用
import os
#项目的根目录
project_path = os.path.dirname(__file__)
#根据项目根目录追加子目录
config_path = os.path.join(project_path,'config')
data_path = os.path.join(project_path,'data')
methods_path = os.path.join(project_path,'methods')
test_case_path = os.path.join(project_path,'test_case')
report_path = os.path.join(project_path,'report')
request_list_path = os.path.join(project_path,'request_list')
log_path = os.path.join(project_path,'log日志')
html_path = report_path+r'\html'
xml_path = report_path+r'\xml'
datayaml_path = data_path+r'\data.yaml'
pytest_path = project_path+r'\pytest.ini'
pytest.ini
- 编写ini文件,定义需要那个文件包下的那些文件,执行的参数是什么
- testpaths就是文件夹
- python_files就是那些文件test_* 就是以test_开头的所有文件
- addopts就是pytest运行时的参数
[pytest]
testpaths=./test_case
python_files=test_*
addopts= -vs --reruns=1 --clean-alluredir --alluredir=./report/xml
main.py
总运行入口
import os
import pytest
from global_path import *
if __name__ == '__main__':
'''
pytest运行参数在pytest.ini文件中配置
'''
pytest.main()
os.system(f'allure generate {xml_path} -o {html_path} --clean')
至此整个接口自动化框架就编写完成了。
下面我们看jenkins如何可以持续化配置
配置代码根目录
配置定时任务
* * * * *
(五颗星,中间用空格隔开)
第一颗 * 表示分钟,取值0~59
第二颗 * 表示小时,取值0~23
第三颗 * 表示一个月的第几天,取值1~31
第四颗 * 表示第几月,取值1~12
第五颗 * 表示一周中的第几天,取值0~7,其中0和7代表的都是周日
1.每30分钟构建一次:H/30 * * * *
2.每2个小时构建一次:H H/2 * * *
3.每天早上8点构建一次:0 8 * * *
4.每天的8点,12点,22点,一天构建3次:0 8,12,22 * * *
(多个时间点,中间用逗号隔开)
构建
使用cmd命令 python main.py运行总开关文件
构建后的操作
保存,运行。
生成后的allure报告