pytest接口自动化搭建经验
前言:目前公司的主要产品是一个web类型的产品;需要做一些自动化,目前的想法是只做接口自动化,不做ui的一个自动化,目前的思路是先对主流程做正常校验,后期再对每一个接口做校验;
一、版本信息:
python版本:3.8.6
其他用:pip install -r requirements.txt
requirements.txt
- allure-pytest==2.8.18
- allure-python-commons==2.8.18
- atomicwrites==1.4.0
- attrs==20.2.0
- certifi==2020.6.20
- cffi==1.14.2
- chardet==3.0.4
- colorama==0.4.3
- configobj==5.0.6
- configparser==5.0.0
- cryptography==3.1
- enum34==1.1.10
- ffmpy==0.2.3
- gevent==20.9.0
- greenlet==0.4.17
- idna==2.10
- importlib-metadata==1.7.0
- iniconfig==1.0.1
- lxml==4.5.2
- more-itertools==8.5.0
- namedlist==1.8
- packaging==20.4
- pluggy==0.13.1
- py==1.9.0
- pycparser==2.20
- pyOpenSSL==19.1.0
- pyparsing==2.4.7
- pytest==6.0.2
- pytest-html==2.1.1
- pytest-metadata==1.10.0
- pytest-ordering==0.6
- pytest-pythonpath==0.7.3
- pytils==0.3
- PyYAML==5.3.1
- requests==2.24.0
- selenium==3.141.0
- six==1.15.0
- toml==0.10.1
- tools==0.1.9
- urllib3==1.25.10
- zipp==3.1.0
- zope.event==4.5.0
- zope.interface==5.2.0
二、项目结构:
具体模块介绍:
- Common:基础方法文件夹
- assert.py:重写assert方法
- commonMethod.py:存一些常用方法,目前存了循环拿key为xx的value值
- conf.py:读写conf配置方法
- consts.py:计划用来存放全局变量,目前也用不上,可用来运行调试代码
- emailSend.py:发送邮件方法
- log.py:生成日志方法
- multitasks.py:多任务方法,目前还没写
- request.py:请求方法
- yamls.py:读写yaml文件方法
- Conf:存放配置
- conf.ini: 存放配置内容;
- Data:存放程序中需要用到的文件;
- Log:存放日志
- Params:存放测试用例,以yaml形式存储,按模块新建文件夹,存放用例
- Report:存放allure数据及报告
- allure-reports:存放allure数据
- html:allure数据解析成html文件
- Testcase:测试方法,以模块来定义py文件及文件名
- gitignore:上传git时忽略哪些文件
- conftest.py:pytest框架默认文件,可存放fixture配置文件
- requirements.txt: 项目中所用的依赖包;
- run.py:启动文件
三、公共模块代码;
assert.py


- # 重写assert方法
- from Common.log import logger
- class Assert:
- def assertEquals(self, actual, expected):
- """
- :param actual:实际值
- :param expected: 期望值
- :return:
- """
- try:
- assert str(actual) == str(expected)
- logger.info("断言成功,实际值:{} 等于预期值{}".format(actual, expected))
- except AssertionError as e:
- logger.error("断言失败,实际值:{}不等于预期值:{}".format(actual, expected))
- raise e
- def assertTrue(self, actual):
- '''
- :param actual: 实际值
- :return:
- '''
- try:
- assert actual == True
- logger.info("断言成功,实际值:{}为真".format(actual))
- except AssertionError as e:
- logger.error("断言失败,实际值:{}不为真".format(actual))
- raise e
- def assertIn(self, content, target):
- try:
- assert content in target
- logger.info("断言成功,目标文本:{}包含文本:{}".format(target, content))
- except AssertionError as e:
- logger.error("断言失败,目标文本:{}不包含文本:{}".format(target, content))
commonMethod.py


- # 循环拿key为objkey的value值
- def get_value(data, objkey, store_data):
- if isinstance(data, dict):
- for k, v in data.items():
- if k == objkey and v:
- store_data.append(v)
- else:
- if isinstance(v, dict) or isinstance(v, list):
- get_value(v, objkey, store_data)
- elif isinstance(data, list):
- for i in data:
- if isinstance(data, dict) or isinstance(data, list):
- get_value(i, objkey, store_data)
- return store_data
conf.py


- # -*- coding: utf-8 -*-
- # @Time : 2020/6/3 15:48
- # @Author : Jackyuan
- # @File : conf.py
- from configobj import ConfigObj
- import os
- class Config:
- curpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- cfgpath = os.path.join(curpath, "./Conf/conf.ini")
- confObj = ConfigObj(cfgpath, encoding='utf-8')
- @classmethod
- def config(self, section):
- return self.confObj[section]
- @classmethod
- def setToken(self, value):
- self.confObj['TOKEN']['token'] = value
- self.confObj.write()
- @classmethod
- def setMyInfo(self, value):
- self.confObj['MYINFO']['myinfo'] = value
- self.confObj.write()
- if __name__ == '__main__':
- print(Config.config("ENV"))
emailSend.py


- # 封装发送邮件方法
- import smtplib
- from email.mime.text import MIMEText
- from email.header import Header
- from email.mime.multipart import MIMEMultipart
- from Common.conf import Config
- from Common.log import logger
- email_conf = Config.config("EMAIL")
- class SendEmail(object):
- def __init__(self):
- # 连接smtp服务器
- self.smtpObj = smtplib.SMTP_SSL(email_conf['mail_host'], email_conf['port'])
- # 登录smtp服务器
- self.smtpObj.login(email_conf['mail_user'], email_conf['mail_pwd'])
- # 构建不带附件的邮件正文
- def content_email(self, content):
- # 构建文件正文
- message = MIMEText(content, 'plain', 'utf-8')
- # 添加发件人
- message['From'] = Header(email_conf["sender"], 'utf-8')
- # 添加收件人
- message['To'] = Header(",".join(email_conf["receivers"]), "utf-8")
- # 添加邮件主题
- message['subject'] = Header(email_conf["subject"], 'utf-8')
- # 发送邮件
- self.send_email(message)
- # 构建带附件的邮件
- def attr_email(self, content, file_url):
- message = MIMEMultipart()
- # 添加发件人
- message['From'] = Header(email_conf["sender"], 'utf-8')
- # 添加收件人
- message['To'] = Header(",".join(email_conf["receivers"]), "utf-8")
- # 添加邮件主题
- message['subject'] = Header(email_conf["subject"], 'utf-8')
- # 创建邮件正文及附件
- message.attach(MIMEText(content, "plain", "utf-8"))
- # 构造附件1
- att1 = MIMEText(open(file_url, 'rb').read(), 'html', 'utf-8')
- att1["Content-Type"] = 'application/octet-stream'
- # 这里的filename可以任意写,写什么名字,邮件中显示什么名字
- att1["Content-Disposition"] = 'attachment; filename="test.html"'
- message.attach(att1)
- # 发送邮件
- self.send_email(message)
- # 发送邮件
- def send_email(self, message):
- try:
- self.smtpObj.sendmail(email_conf["sender"], email_conf["receivers"], message.as_string())
- except Exception as e:
- logger.error("发送带附件的邮件失败:{}".format(e))
log.py


- # 日志收集设置
- import logging, os
- from logging.handlers import TimedRotatingFileHandler
- import datetime
- current_dir = os.path.abspath(os.path.dirname(__file__))
- parent_dir = os.path.dirname(current_dir)
- my_log_path = os.path.join(parent_dir, "./Log")
- if not os.path.exists(my_log_path):
- os.mkdir(my_log_path)
- my_report_path = os.path.join(parent_dir, "./Report")
- if not os.path.exists(my_report_path):
- os.mkdir(my_report_path)
- # 定义一个日志收集器
- logger = logging.getLogger("guoguo")
- # 设置收集器级别,不设定的话,会默认搜集warning及以上级别的日志
- logger.setLevel(logging.INFO)
- # 设置日志格式
- fmt = logging.Formatter("%(filename)s-%(lineno)d-%(asctime)s-%(levelname)s-%(message)s")
- # 设置日志输出到控制台
- stream_handler = logging.StreamHandler()
- # 设置日志输出到文件
- file_handler = TimedRotatingFileHandler('Log/{}.log'.format(datetime.datetime.now().strftime('%Y-%m-%d')), when="D", interval=1, backupCount=30, encoding='utf-8')
- # 设置控制台和文件的日志输出格式
- stream_handler.setFormatter(fmt)
- file_handler.setFormatter(fmt)
- # 将输出对象添加到logger中
- logger.addHandler(file_handler)
- logger.addHandler(stream_handler)
- """
- 收集日志
- logger.info()
- logger.debug()
- logger.warning()
- logger.error()
- """
requests.py


- # 封装请求方法
- import requests
- from Common.log import logger
- from Common.conf import Config
- # 获取配置文件中的token
- token = {"token": Config.config("TOKEN")['token']}
- class RequestMethod(object):
- # 请求日志
- def api_log(self, method, url, headers=None, params=None, data=None, files=None, code=None, res_header=None, res_text=None, time=None):
- logger.info("请求方式===>{}".format(method))
- logger.info("请求路径===>{}".format(url))
- logger.info("请求头===>{}".format(headers))
- logger.info("请求参数===>{}".format(params))
- logger.info("请求体===>{}".format(data))
- logger.info("上传的文件内容===>{}".format(files))
- logger.info("响应状态码===>{}".format(code))
- logger.info("响应头===>{}".format(res_header))
- logger.info("响应体===>{}".format(res_text.decode("utf-8")))
- logger.info("响应时间===>{}".format(time))
- # get请求
- def get_main(self, url, headers, params=None):
- try:
- response = requests.get(url=url, headers=headers, params=params)
- return response
- except Exception as e:
- logger.error("{}该路径get请求出错,错误原因{}".format(url, e))
- # post请求
- def post_main(self, url, headers, params=None, data=None, files=None):
- try:
- response = requests.post(url=url, headers=headers, params=params, data=data, files=files)
- return response
- except Exception as e:
- logger.error("{}该路径post请求出错,错误原因:{}".format(url, e))
- # put请求
- def put_main(self, url, headers, params=None, data=None, files=None):
- try:
- response = requests.put(url=url, headers=headers, params=params, data=data, files=files)
- return response
- except Exception as e:
- logger.error("{}该路径put请求出错,错误原因{}".format(url, e))
- # delete请求
- def delete_main(self, url, headers, params=None):
- try:
- response = requests.delete(url=url, headers=headers, params=params)
- return response
- except Exception as e:
- logger.error("{}该路径delete请求出错,错误原因{}".format(url, e))
- # 选择调用方法
- def run_main(self, method, url, headers=None, data=None, params=None, files=None):
- if headers is None:
- headers = token
- else:
- headers.update(token)
- if method == "get":
- response = self.get_main(url, headers, params=params)
- elif method == "post":
- response = self.post_main(url, headers, params=params, data=data, files=files)
- elif method == "put":
- response = self.put_main(url, headers, params=params, data=data, files=files)
- elif method == "delete":
- response = self.delete_main(url, headers, params=params)
- else:
- logger.error("请求方式有误")
- self.api_log(method=method, url=response.url, headers=headers, params=params, data=data, files=None, code=response.status_code, res_header=response.headers, res_text=response.content, time=response.elapsed.total_seconds())
- return response
yamls.py


- import yaml
- from Common.log import logger
- class ReadYaml(object):
- def __init__(self, file_url, write_data=None):
- self.file_url = file_url
- self.write_data = write_data
- # 读取yaml数据
- def read_yaml(self):
- file_data = None
- try:
- with open(self.file_url, "r", encoding='utf-8') as f:
- file_data = yaml.safe_load(f.read())
- logger.info("读取{}数据成功,读取数据为:\n{}".format(self.file_url, file_data))
- except Exception as e:
- logger.error("{}读取失败,失败原因为:{}".format(self.file_url, e))
- return file_data
- # 向yaml写入数据
- def write_yaml(self, method):
- if method == "append":
- method = "a+"
- elif method == "write":
- method = 'w+'
- else:
- return "method錯誤"
- try:
- with open(self.file_url, method, encoding="utf-8") as f:
- yaml.dump(self.write_data, f)
- logger.info("写入数据成功,写入后数据为:\n{}".format(self.file_url, self.read_yaml()))
- except Exception as e:
- logger.error("{}写入数据失败,失败原因为:{}".format(self.file_url, e))
- return self.read_yaml()
pytest接口自动化搭建经验的更多相关文章
- Pytest(18)pytest接口自动化完整框架思维导图
pytest接口自动化完整框架思维导图
- python+pytest接口自动化(11)-测试函数、测试类/测试方法的封装
前言 在python+pytest 接口自动化系列中,我们之前的文章基本都没有将代码进行封装,但实际编写自动化测试脚本中,我们都需要将测试代码进行封装,才能被测试框架识别执行. 例如单个接口的请求代码 ...
- python+pytest接口自动化(12)-自动化用例编写思路 (使用pytest编写一个测试脚本)
经过之前的学习铺垫,我们尝试着利用pytest框架编写一条接口自动化测试用例,来厘清接口自动化用例编写的思路. 我们在百度搜索天气查询,会出现如下图所示结果: 接下来,我们以该天气查询接口为例,编写接 ...
- python+pytest接口自动化(13)-token关联登录
在PC端登录公司的后台管理系统或在手机上登录某个APP时,经常会发现登录成功后,返回参数中会包含token,它的值为一段较长的字符串,而后续去请求的请求头中都需要带上这个token作为参数,否则就提示 ...
- python+pytest接口自动化(16)-接口自动化项目中日志的使用 (使用loguru模块)
通过上篇文章日志管理模块loguru简介,我们已经知道了loguru日志记录模块的简单使用.在自动化测试项目中,一般都需要通过记录日志的方式来确定项目运行的状态及结果,以方便定位问题. 这篇文章我们使 ...
- python+pytest接口自动化
本篇文章是用python+pytest写了一个简单的接口自动化脚本,外加循环请求接口的语法,大家可以参考~ 实例一: import requestsimport pytestimport time c ...
- python+pytest接口自动化(4)-requests发送get请求
python中用于请求http接口的有自带的urllib和第三方库requests,但 urllib 写法稍微有点繁琐,所以在进行接口自动化测试过程中,一般使用更为简洁且功能强大的 requests ...
- python+pytest接口自动化(6)-请求参数格式的确定
我们在做接口测试之前,先需要根据接口文档或抓包接口数据,搞清楚被测接口的详细内容,其中就包含请求参数的编码格式,从而使用对应的参数格式发送请求.例如某个接口规定的请求主体的编码方式为 applicat ...
- python+pytest接口自动化(9)-cookie绕过登录(保持登录状态)
在编写接口自动化测试用例或其他脚本的过程中,经常会遇到需要绕过用户名/密码或验证码登录,去请求接口的情况,一是因为有时验证码会比较复杂,比如有些图形验证码,难以通过接口的方式去处理:再者,每次请求接口 ...
- python pytest接口自动化框架搭建(一)
1.首先安装pytest pip install pytest 2.编写单测用例 在pytest框架中,有如下约束: 所有的单测文件名都需要满足test_*.py格式或*_test.py格式. 在单测 ...
随机推荐
- Python实现企业微信自动打卡程序二:跳过节假日,随机打卡时间,定时任务,失败通知
一.介绍 在上节 Python实现企业微信上下班自动打卡程序内容之后,我们继续优化自动打卡程序.接下来增加如下内容: 实现打卡时间随机范围 处理节假日不打卡的情况 实现定时调度打卡 打卡成功或失败通知 ...
- Spring事务(六)-只读事务
@Transactional(readOnly=true)就可以把事务方法设置成只读事务.设置了只读事务,事务从开始到结束,将看不见其他事务所提交的数据.这在某种程度上解决了事务并发的问题.一个方法内 ...
- Error running 'Tomcat 8.5.27': Unable to open debugger port (127.0.0.1:2887): java.net.SocketException "Interrupted function call: accept failed"-火绒安全搞的鬼
火绒安全-导致的tomcat8启动异常 一.问题由来 最近有个朋友在学习使用IDEA配置tomcat 8.5.99的时候,使用一切都正常,直到学习到使用Servlet实现文件 下载功能的时候,出现问题 ...
- vim技巧--提取文本与文本替换
前几天遇到一个使用情景,需要从一个包含各个读取代码文件路径及名字的文件中把文件路径提取出来,做一个filelist,这里用到了文本的提取和替换,这里做个小总结记录一下. 从网上找了一个作者写的代码用来 ...
- PlacementList must be sorted by first 8 bits of display_id 问题
问题暂未解决 [37484:0811/103448.115:ERROR:display_layout.cc(551)] PlacementList must be sorted by first 8 ...
- vue 动态加载css,改变网站皮肤模式
Vue.mixin({ created () { require('view-design/dist/styles/iview.css') } }) 参考资料:https://blog.csdn.ne ...
- think about 和 think of 区别
about 是 on by out 简称 about 在旁边 在外围 周边 think about you 想你有关的事 of 是 belong to 什么什么的 of指的是 这个人或者这个事本身相关 ...
- 基于泰凌微的TLSR8355芯片的2.4G无线私有协议PCBA设计调试总结
一 前记 经常做物联网的类的产品,TLSR8355凭借着它的射频距离远,功能强大等优点成为很多客户的首选.TLSR8355系列专用于2.4GHz射频系统芯片解决方案,如零售/物流.专用网络.Beaco ...
- 记录--Vue中使用websocket的正确姿势
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 1:首先谈谈websocket是什么? WebSocket是一种在单个TCP连接上进行全双工通信的协议.WebSocket通信协议于201 ...
- 一键解决App应用分发下载问题
本文深入分析了App应用分发下载失败的常见原因,并提供了针对网络连接问题.服务器故障.设备存储空间不足.文件大小和格式不受支持等方面的解决方法.此外,还附带了一个在线证书制作工具的案例演示,旨在帮助开 ...