python + requsts 接口测试框架的分层设计与ddt(数据驱动)
分成设计如下图的三层结构(也可以设计成两层,注:分层设计仅仅是一个很简单的设计模式)
- common包存放的是路径处理模块、配置文件处理模块,数据处理模块等等。以下以该框架举例。
- connectdb.py模块是对数据库连接的封装其内容如下:
# -*- coding: utf-8 -*-
"""
***********************
@author: - FrankLee(李学春)
@time: - 2020/11/4
@qq: - 775729278
@wechat: - lxc18286562925
@FileName: - connectdb.py
***********************
"""
"""
连接数据库
"""
import pymysql, pymongo, redis, pymssql
from common.handleconfig import conf
class DbUtils(object):
def __init__(self, sql):
self.sql = sql
"""
判断数据库类型
"""
if self.sql == "mysql":
'''
连接mysql数据库
'''
self.conn = pymysql.connect(host=conf.get("db", "host"),
ussr=conf.get("db", "user"),
password=conf.get("db", "password"),
database=conf.get("db", "database"),
port=conf.get("db", "port")
)
'''
获取游标对象
'''
self.cur = self.conn.cursor()
elif self.sql == "mongodb":
self.conn = pymongo.MongoClient()
elif self.sql == "redis":
self.conn = redis.Redis()
elif self.sql == "orcal":
pass
else:
print("数据库类型错误")
def find_one(self, sql):
"""
查询一条数据
:param sql: sal语句
:return:
"""
self.cur.execute(sql)
return self.cur.fetchone()
def find_all(self, sql):
"""
查询所有数据
:param sql: sql语句
:return:
"""
self.cur.execute(sql)
return self.cur.fetchall()
def close_all(self):
"""
关闭游标和连接
:return: None
"""
self.cur.close()
self.conn.close()
# 以上只是一个半成品,只是对mysql的一个封装,其中的mongodb,redis、orcal、sqlserver等未写完
# 封装方法大同小异
-
- handleconfig.py 模块是对配置文件的处理,其内容如下(结合conf下的config.ini来理解):
# -*- coding: utf-8 -*-
"""
***********************
@author: - FrankLee(李学春)
@time: - 2020/11/4
@qq: - 775729278
@wechat: - lxc18286562925
@FileName: - handleconfig.py
***********************
"""
"""
处理config的配置信息
导入configparser下的ConfigPaser 处理ini文件
"""
from configparser import ConfigParser
from common.handlepath import *
class HandleConfig(ConfigParser):
def __init__(self, filename):
"""
先继承父类的构造函数
"""
super().__init__()
self.filename = filename
"""
读取文件
"""
self.read(filename)
def write_data(self, section, option, value=None):
"""
向配置文件ini上写如信息
"""
self.set(section, option, value)
"""
将内容写到文件里去
"""
self.write(fp=open(self.filename))
conf = HandleConfig(os.path.join(CONFDIR, "config.ini"))
-
- handleemail.py模块是用来封装邮件发送的,其内容如下:
# -*- coding: utf-8 -*-
"""
***********************
@author: - FrankLee(李学春)
@time: - 2020/11/4
@qq: - 775729278
@wechat: - lxc18286562925
@FileName: - handleconfig.py
***********************
"""
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from common.handleconfig import conf
def send_email(filename, title):
"""
发送邮件的功能函数
:param filename: 文件的路径
:param title: 邮件的主题
:return:
"""
"""
第一步:连接邮箱的smtp服务器,并登录
"""
smtp = smtplib.SMTP_SSL(host=conf.get("email", "host"),
port=conf.getint("email", "port"))
smtp.login(user=conf.get("email", "user"),
password=conf.get("email", "pwd"))
"""
第二步:构建一封邮件
创建一封多组件的邮件
"""
msg = MIMEMultipart()
with open(filename, "rb") as f:
content = f.read()
"""
创建邮件文本内容
"""
text_msg = MIMEText(content, _subtype="html", _charset="utf8")
"""
添加到多组件的邮件中
"""
msg.attach(text_msg)
"""
创建邮件的附件
"""
report_file = MIMEApplication(content)
report_file.add_header('content-disposition', 'attachment',
filename=os.path.split(filename)[-1])
"""
将附件添加到多组件的邮件中
"""
msg.attach(report_file)
"""
主题
"""
msg["Subject"] = title
"""
发件人
"""
msg["From"] = conf.get("email", "from_addr")
"""
收件人
"""
msg["To"] = conf.get("email", "to_addr")
"""
第三步:发送邮箱
"""
smtp.send_message(msg, from_addr=conf.get("email", "from_addr"),
to_addrs=conf.get("email", "to_addr"))
-
- handlelog.py 模块封装的是对日志的处理,内容如下:
# -*- coding: utf-8 -*-
"""
***********************
@author: - FrankLee(李学春)
@time: - 2020/11/5
@qq: - 775729278
@wechat: - lxc18286562925
@FileName: - handlelog.py
***********************
"""
import logging
import os
from common.handleconfig import conf
from common.handlepath import LOGDIR
class HandleLog(object):
@staticmethod
def create_loger():
"""
创建日志收集,设置日志等级
:return:
"""
mylog = logging.getLogger(conf.get("log", "name"))
mylog.setLevel(conf.get("log", "level"))
"""
创建输出到控制台的日志等级,
并添加到日志收集器
"""
sh = logging.StreamHandler()
sh.setLevel(conf.get("log", "sh_level"))
mylog.addHandler(sh)
"""
创建输出到文件的日志等级
"""
fh = logging.FileHandler(os.path.join(LOGDIR, "log.log"))
fh.setLevel(conf.get("log","fh_level"))
mylog.addHandler(fh)
"""
定义输出日志的格式
"""
formater = "%(asctime)s - [%(filename)s-->line:%(lineno)d] - %(levelname)s:%(message)s"
fm = logging.Formatter(formater)
sh.setFormatter(fm)
fh.setFormatter(fm)
return mylog
log = HandleLog.create_loger()
-
- handlepath.py 是对整个项目路径的封装,有利于移植和后续的操作,内容如下:
# -*- coding: utf-8 -*-
"""
***********************
@author: - FrankLee(李学春)
@time: - 2020/11/4
@qq: - 775729278
@wechat: - lxc18286562925
@FileName: - handlepath.py
***********************
"""
import os
# 定义项目的路径
BASEDIR = os.path.dirname(os.path.dirname(__file__))
# 定义conf配置文件的路径
CONFDIR = os.path.join(BASEDIR, "conf").replace("\\", "/")
# 定义data的路径
DATADIR = os.path.join(BASEDIR, "data").replace("\\", "/")
# 定义log的路径
LOGDIR = os.path.join(BASEDIR, "log").replace("\\", "/")
# 定义report的路径
REPORTDIR = os.path.join(BASEDIR, "report").replace("\\", "/")
# 定义testcase的路径
TESTDIR = os.path.join(BASEDIR, "testcase").replace("\\", "/")
if __name__ == "__main__":
print(BASEDIR)
print(CONFDIR)
print(DATADIR)
print(LOGDIR)
print(REPORTDIR)
print(TESTDIR)
-
- handlerequests.py 模块是对requests库的再次封装,以便减轻后续代码量,内容如下:
# -*- coding: utf-8 -*-
"""
***********************
@author: - FrankLee(李学春)
@time: - 2020/11/4
@qq: - 775729278
@wechat: - lxc18286562925
@FileName: - handlerequests.py
***********************
"""
"""
处理requsts请求
"""
import requests
class SendRequest(object):
def __init__(self):
"""
让登录和登录后的接口保持在同一个会话当中,上下文管理
"""
self.session = requests.Session()
def send(self, method, url, params=None, data=None, headers=None, json=None, cookies=None, files=None):
"""
:param method: 请求方法
:param url: 请求地址
:param params: get请求的参数
:param data: post请求application/x-www-form-urlencoded格式的参数
:param headers: 请求头部信息
:param json: post请求json格式
:param cookies:
:param files: 请求上穿文件
:return:
"""
"""
将请求方法转变为小写
"""
method = method.lower()
"""
判断请求方法
"""
try:
if method == "get":
resp = self.session.get(url, params=params, headers=headers)
elif method == "post":
resp = self.session.post(url=url, data=data,headers=headers)
except Exception as e:
print(e)
else:
if method == "get":
resp = self.session.get(url, params=params, headers=headers, verify=False)
elif method == "post":
resp = self.session.post(url=url, data=data,headers=headers, verify=False)
return resp
-
- readexcel.py 模块是要读取excel中的用例和测试数据,其内容如下:
# -*- coding: utf-8 -*-
"""
***********************
@author: - FrankLee(李学春)
@time: - 2020/11/4
@qq: - 775729278
@wechat: - lxc18286562925
@FileName: - readexcel.py
***********************
"""
"""
读取excel的数据
"""
import openpyxl
import os
from common.handlepath import *
class ReadExcel(object):
def __init__(self, filename, sheetname):
"""
:param filename: excel的名称
:param sheetname: 工作簿的名称
"""
self.filename = filename
self.sheetname = sheetname
def __open(self):
"""
打开excel表格
:return:
"""
self.wb = openpyxl.load_workbook(self.filename)
self.sh = self.wb[self.sheetname]
def readData(self):
"""
打开工作簿,获取数据
:return:
"""
self.__open()
"""
取每一行的数据当到元组中
"""
datas = list(self.sh.rows)
"""
获取表头的数据
"""
tile = [i.value for i in datas[0]]
"""
定义一个列表来接收所有的用例
"""
cases = []
for i in datas[1:]:
"""
把title变成键,把后边的变成值
"""
case = dict(zip(tile, [j.value for j in i]))
cases.append(case)
return cases
def writeData(self, row, column, value):
"""
数据回写
:return:
"""
self.__open()
self.sh.cell(row, column, value)
self.wb.save(self.filename)
- conf 目录存放的是,配置文件,其包括日志、邮箱、测试环境、数据、数据库信息等等:
-
- config.ini 是配置文件,编写方式如下:
[log]
name = frank
level = DEBUG
sh_level = ERROR
fh_level = INFO
[env]
url = http://www.baidu.com
headers = {"Content-Type":"application/x-www-form-urlencoded"}
[test_data]
username = frank
password = 1234567
[db]
host = 118.24.119.60
user = root
password = 123456
database = cms
port = 3306
[email]
- data 目录存放的是测试用例的excel文件
-
- apicase.xlsx 存放的是测试用例,其内容如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4tQa00qz-1607655257199)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20201105155205820.png)]
- library 包存放的是一些 如生成报告、或者是数据驱动用到的包,内容就不列出:
- log 目录存放的是日志文件
- report 目录存放的是报告或者截图等
- testCase 包存放的就是具体的自动化测试用例
-
- 已testLogin.py 模块 来举例,内容如下:
# -*- coding: utf-8 -*-
"""
***********************
@author: - FrankLee(李学春)
@time: - 2020/11/5
@qq: - 775729278
@wechat: - lxc18286562925
@FileName: - testLogin.py
***********************
"""
import unittest
import os
from common.handlerequests import SendRequest
from common.handlelog import log
from common.readexcel import ReadExcel
from common.handlepath import DATADIR
from common.handleconfig import conf
from library.ddt import data, ddt
casefile = os.path.join(DATADIR, "apicase.xlsx").replace("\\", "/")
"""
使用ddt装饰类,作数据驱动
"""
@ddt
class TestLogin(unittest.TestCase):
"""
读取excel的用例
"""
excel = ReadExcel(casefile, "login")
cases = excel.readData()
request = SendRequest()
"""
使用ddt 的data()函数装饰用例
接收可变长参数
"""
@classmethod
def setUpClass(cls):
print("测试开始")
@classmethod
def tearDownClass(cls):
print("测试接收")
@data(*cases)
def test001Login(self, case):
"""
1、准备接口请求数据
:param case:
:return:
"""
url = conf.get("env", "url") + case["url"]
method = case["method"]
data = eval(case["data"])
headers = eval(conf.get("env", "headers"))
expect = eval(case["expected"])
"""
每执行一次让id加1
"""
row = case["case_id"] + 1
"""
发送接口请求
"""
resp = self.request.send(method, url, data=data, headers=headers)
dic = eval(resp.text)
try:
self.assertEqual(dic, expect, "测试不通过")
except Exception as e:
self.excel.writeData(row, 8, "不通过")
log.error("用例{}执行未通过,case_id为{}".format(case["title"], row))
raise e
else:
self.excel.writeData(row, 8, "通过")
log.info("用例{}执行通过,case_id为{}".format(case["title"], row))
“”
row = case[“case_id”] + 1
"""
发送接口请求
"""
resp = self.request.send(method, url, data=data, headers=headers)
dic = eval(resp.text)
try:
self.assertEqual(dic, expect, "测试不通过")
except Exception as e:
self.excel.writeData(row, 8, "不通过")
log.error("用例{}执行未通过,case_id为{}".format(case["title"], row))
raise e
else:
self.excel.writeData(row, 8, "通过")
log.info("用例{}执行通过,case_id为{}".format(case["title"], row))