说实在的,内容很长,快相当于出书了,这里是我自己搭建pytest自动化框架的整个过程,里面有踩过的坑,也走过弯路,也有用最简单粗暴的编码方式。不过总归是在不断优化封装方法的过程中得到了提升,希望看完后对你一样可以有所帮助。
pytest安装
pytest基础使用介绍
核心技术:
自动化实战项目介绍
数据驱动封装
接口调用方法封装
不断更新常用封装方法
pytest是一个非常成熟的自动化框架,比其他框架更灵活更容易上手
pytest可以和selenium,request,appium结合实现web自动化,接口自动化,app自动化
pytest可以时间testcase的跳过以及是失败重试
pytest可以和allure生成美观报告
pytest可以和jenkins持续集成
在cmd中输入pip install -U pytest
pytest
pytest-html(生成测试报告)
pytest-xdist(测试用例分布式执行,多CPU分发)
pytest-ordering(用于改变测试用例的执行顺序)
pytet-rerunfaileures(用例失败后重跑)
allure-pytest(生成美观的测试报告)
base_url (定义域名全局变量)pip install pytest-base-url
用pip install -U 插件名 安装以上插件
默认规则:
模块名必须以test_开头或者_test结尾
测试类必须以Test开头,并且不能有int方法
测试方法必须以test开头
配置文件:
pytest.ini
[pytest]
#--html ./report/report.html #生成html报告到./report目录
addopts = -vs --alluredir ./temp
#运行加参数 -vs显示详细信息,打印调试信息
#--alluredir ./temp 生成alluredir 的json临时报告到temp文件下
testpaths = ./test_case #测试case路径
python_files = test*.py #模块名规则
python_classes = Test* #类名规则
python_functions = test #方法名规则
pytest-html(生成测试报告)
allure-pytest(生成美观的测试报告)
if __name__ == '__main__':
pytest.main(['path/文件名.py'])
os.system('allure generate ./temp -o ./report --clean')
数据驱动
关键字驱动
全局配置文件封装
日志监控
断言
报告邮件…
import pytest
import os
if __name__ == '__main__':
pytest.main(['path/文件名.py'])
# pytest.main(['-vs'])
os.system('allure generate ./temp -o ./report --clean')
yaml格式
excel格式
csv格式
json格式
mysql
1.区分大小写
2.和python一样也是通过缩进的方式来表示层级关系(不同的是不能使用tab缩进,只能用空格缩进)
3.和缩进多少层无关,只和左边是否对齐有关系
4.#表示注释
1.map对象 键:(空格)值
eg:
name: 小明
2.数组(列表) :用一组横线开头
eg:
-name: 小明
-name: 小红
import yaml
def read_yaml():
"""
读取yaml文件
:return:
"""
with open('文件名',encoding='utf-8') as f:
data = yaml.load(f,loader=yaml.FullLoader)
return data
if __name__=='__main__':
print(read_yaml())
def RedJson(jsonurl):
# ss = os.path.dirname(os.path.abspath(__file__)) 拿到json路径
f = os.path.join(jsonurl, 'test.json')
dict_data = {}
with open(f, 'r', encoding="utf-8") as fp:
dict_data = json.load(fp)
c_f = os.path.split(os.path.abspath(__file__))[1]
return list(dict_data.values())[0]
import requests
def test_baidu():
url = 'http://www.baid.com'
json={
"account": 1, "pageNum": 3, "pageSize": 4
}
r = requests.post(url=url,json=json) #url请求地址,json请求入参
print(r.text) #以text格式输出返回值
assert r.status_code == 200 #断言
url的环境域名配置成全局参数
json存储请求数据
from config.config import serverip #全局参数含url
from common.excel_value import get_excel_value #调用excel拿到请求参数
import requests #引入requests模块
import os #引入os系统模块
from common.getjson import RedJson #引入获取json数据公共方法
import pytest #引入pytest模块
jsonurl = os.path.dirname(os.path.abspath(__file__))
#将当前模块目录下的json文件地址传给获取json数据的公共方法,拿到json数据
@pytest.fixture(scope="function")
def basic_data():
return RedJson(jsonurl)
#使用parametrize装饰器入参
@pytest.mark.parametrize('account, pageNum, pageSize',
[
(1, 2, 3),
(1, 2, 3),
])
def test_queryList(account, pageNum, pageSize, basic_data):
url = serverip() + 'path地址/queryList'
json = {
"account": account, "designId": basic_data['designId'], "pageNum": pageNum, "pageSize": pageSize}
r = requests.post(url=url, json=json)
print(r.text)
assert r.status_code == 200
配置各环境域名,或者其他公共参数
缺点:运行时候,还需要手动修改参数来改变环境域名。
或许还有其他封装方法,但是还没实现。
def serverip():
'''
DEV= 开发环境的服务器ip
QA= 测试环境的服务器ip
OL= 线上环境的服务器ip
:return: 返回不同服务器的地址
'''
server_add={
'DEV' : 'http://his.xxxxxxxxxxx.com',
'QA' : 'http://www.baidu.qa/',
'OL' : 'http://www.baidu.ol/'
}
return server_add['QA']
使用base_url插件,上面有引用方法
优点:
运行时候,只需要在执行命令中加上环境域名就可以,不需要手动更改代码参数
缺点:
只能在test_*函数中引用
# test_demo.py
import requests
def test_example(base_url):
assert 200 == requests.get(base_url).status_code
运行1:
命令行执行的时候加上 --base-url 参数
pytest —base-url http://www.baidu.com
运行2:
也可以在 pytest.ini 配置文件中添加 base_url 地址
# pytest.ini文件内容
[pytest]
base_url = http://www.example.com
这样在命令行执行时候就可以不用带上 --base-url 参数
定义接口:
import requests
class Module:
def __init__(self, env_data):
#将字典{"base_url": base_url}传给Module初始化参数
self.__dict__.update(env_data)
#Module实例参数化 self.base_url : base_url 也就是“http://www.baidu.com”
def queryList(self,account,dId,pageNum,pageSize):
url = self.base_url + /newspage/data/landingsuper #这个path地址也可以封装,详情看接口封装
json ={
"account": account, "dId": dId, "pageNum": pageNum, "pageSize": pageSize
}
r = requests.post(url=url,json=json)
return r
#test_case
import pytest
引用上面封装的Module类
#使用fixture装饰器封装,拿到base_url进行参数化
@pytest.fixture(scope="function")
def basic_data(base_url):
# print(base_url)
return Module({"base_url": base_url})
#返回Module实例,也就是basic_data = Module类的实例
def test_queryList(basic_data):
a = basic_data.queryList(2,2,2,3) #basic_data=Module实例
print(a)
assert a.status_code == 200
作用:不同环境下,全局参数不一样,切换环境,无需修改代码,只需要在执行命令时候加对应环境参数就可以获取到指定环境的全局参数。
优点:可以自定义命令参数,在命令执行时候,直接加对应的参数就可以取到对应环境的全局参数
自定义命令参pytest_addoption传参简介:
name:自定义命令行参数的名字,可以是:“foo”, “-foo” 或 “–foo”;
action:在命令行中遇到此参数时要采取的基本操作类型;
nargs:应该使用的命令行参数的数量;
const:某些操作和nargs选择所需的常量值;
default:如果参数不在命令行中,则生成的默认值。
type:命令行参数应该转换为的类型;
choices:参数允许值的容器;
required:命令行选项是否可以省略(仅可选);
help:对参数作用的简要说明;
metavar:用法消息中参数的名称;
dest:要添加到 parse_args() 返回的对象中的属性的名称;
action=“store”:默认,只存储参数的值,可以存储任何类型的值,此时 default 也可以是任何类型的值,而且命令行参数多次使用也只能生效一个,最后一个值覆盖之前的值;
根目录下创建一个conftest.py文件
# conftest.py
import pytest
from common.getTxt import read_txt #调用封装好的读取TXT文件并存成字典格式
import os
#获取项目绝对路径
project_name = "InterfaceTest_project"
curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = curPath[:curPath.find(project_name+"\\")+len(project_name+"\\")]
#关键点!!拿到项目所在的绝对路径
#需要把获取到项目根路径中“\”(用print看到的是"\\")替换测“/”
rootPath = str(rootPath).replace("\\","/")
firedir='{}reports/mypies'.format(rootPath)
def pytest_addoption(parser):
parser.addoption("--env", action="store", default="qa",
help="不传值默认qa环境参数,传ol,pl分别取对应环境参数")
@pytest.fixture()
def env(request):
s = request.config.getoption("--env")
if s == "ol": #执行case命令时候,--env=ol,就取ol的全局参数
env = read_txt("D:\config\ol.txt")
#存储ol全局参数文件url,这里是写死的路径,如果多人合作或者更换项目路径则不适用。所以需要用相对路径来入参。详情看qa的路径
elif s == "pl": #执行case命令时候,--env=pl,就取pl的全局参数
env = read_txt("D:\config\pl.txt")
else: #执行case命令时候,不传值,或者传值不是ol,pl,就取qa的全局参数
env = read_txt(rootPath +"/项目名/../config/qa.txt") #拿到项目绝对路径和文件在项目中的目录进行拼接
return env
封装的调用getTxt方法:
# coding=utf-8
import codecs
def read_txt(path):
txt_dict = {}
with codecs.open(path, 'r', 'utf-8') as config:
for line in config.readlines():
# 遍历文件中每一行并以“=”分隔,再做列表解析,使用for循环去掉换行符,并以列表形式返回
result = [ele.strip() for ele in line.split('=', 1)]
# 先使用dict()将嵌套列表'[result]'转换成字典,再使用update更新字典
txt_dict.update(dict([result]))
return txt_dict
存储QA环境全局参数的txt文件
#qa.txt
#全局参数存储地方
#参数名=值
dId=101
aaaa=22222
bbb=333
执行case命令:
pytest 自定义命令名=需要传的值
pytest --env=qa
pytest --env=pl
pytest --env=ol
原始的使用方式:
封装方法:
class PageModule:
def __init__(self, env_data):
self.__dict__.update(env_data)
def page_add(self, configTemplateName, pageAdjuster, designId):
url = self.url + page.page_add
json = {
"configTemplateName": configTemplateName, "pageAdjuster": pageAdjuster, "designId": designId
}
r = requests.post(url=url, json=json)
return r
case使用:
from common.api_instance.management.page import PageModule
from common.data_set import basic_json
from common.Logging import logger
class Testpage():
logger.info("验证页面模板主流程")
page_all = basic_json()
def test_page_add(self, env):
logger.info("step1: 可以正常添加页面模板数据")
response = PageModule({"url": env.get('url')}).page_add(
Testpage.page_all["page-all"][0]["configTemplateName"],
Testpage.page_all["page-all"][0]["pageAdjuster"],
env.get("designId"))
assert response.status_code == 200
assert "处理成功" in response.text
进阶版使用:
封装初始化参数的BasicService方法
将全局变量env通过类继承方式传参
class BasicService(object):
"""将基础数据(env)传给封装的接口类并继承做初始化数据"""
def __init__(self, env_data: dict = {}):
self.__dict__.update(env_data)
定义接口封装:
self.*都是全局变量存在env中的
引用BasicService
from common.basic import BasicService
from common.path_enum.service_path import pageDecoration
class PageDecorationModule(BasicService):
def queryPageDecorationInfo(self, data_set: dict):
"""获取装修页信息"""
url = self.url + pageDecoration.queryPageDecorationInfo
json = {
"bosId": self.bosId, "merchantId": self.merchantId, "pageId": data_set.get('pageId'),
"tcode": self.tcode, "type": self.type, "vid": self.vid
}
r = requests.post(url=url, json=json)
return r
case调用:
class TestpageDecoration():
logger.info("验证页面装修主流程")
def test_queryPageDecorationInfo(self, env):
logger.info("获取页面信息,判断页面名字存在")
response = PageDecorationModule(env_data=env).queryPageDecorationInfo({"pageId": TestpageDecoration.getpageId})
assert response.status_code == 200
assert "处理成功" in response.text
common —存储公共方法
config —存储公共参数
report —存储报告
temp —存储临时文件
test_case —测试case
test_case中,各个模块下面存一个json文件,用于存储该模块下用到的参数
test_data --存储测试数据(excel,CSV驱动数据)
pytest.ini —pytest的核心配置,上面有介绍
run.py —运行主方法
一定要有面向对象的编程思路,该封装的一定要进行封装,尽量不要出现硬编码
将接口封装为公共调用方法,实现了面向对象编程。这样再写case时候可以大量缩短代码行数,别人也可以直接调用封装的方法。
层级划分:(仅供参考,按业务需求设计)
common
api_instence
运营端(服务端)
模块名1.py #一个py文件存储该模块下所有接口的封装
模块名2.py
*****
B端(服务端)
模块名a.py
*****
C端(服务端)
模块名A.py
*****
enum
运营端_path.py #一个py文件存储该模块下的所有path地址
B端_path.py
C端_path.py
test_case
服务端1
模块文件1
接口case.py
模块文件2
*******
服务端2
模块文件a
接口case.py
按服务端存储path地址
#一个class类代表一个模块,存储一个模块的接口path
#创建类1
class 模块名class1:
"""
queryInfo=查询详情接口path
add=新增机接口path
*******
"""
queryInfo = "aaa/bbbbbb/ccccc/queryInfo"
add = "/aaaaa/bbbbbb/cccccc/add"
*******
#创建类2
class 模块名class2:
"""
addAccount = 新增账号接口
*******
"""
addAccount = "/dddd/cccc/bbbb/addAccount"
按模块接口封装
#模块名1.py
from common.enum.运营端_path import 类名 #引用接口path类,目的是获取接口的path地址
import requests
class Module:
def __init__(self, env_data):
#将字典{"base_url": base_url}传给Module初始化参数
self.__dict__.update(env_data)
#Module实例参数化 self.base_url : base_url 也就是“http://www.baidu.com”
#接口1封装
def queryList(self,account,dId,pageNum,pageSize):
url = self.base_url + AbleModuleSet.queryList
json ={
"account": account, "dId": dId, "pageNum": pageNum, "pageSize": pageSize
}
r = requests.post(url=url,json=json)
return r
#接口2封装
def queryInfo(self,moduleSetId,dId):
url = self.base_url + AbleModuleSet.queryList
json ={
"moduleSetId": moduleSetId, "dId": dId
}
r = requests.post(url=url,json=json)
return r
test_case执行
#test_case.py
import os
import pytest
from common.api_instence.服务端名.模块名 import Module #引用封装的接口类,目的是可以直接调用接口
#获取当前路径的json文件路径给到 jsonul,获取json里面的数据,数据驱动时候用到,上面有介绍
jsonurl = os.path.dirname(os.path.abspath(__file__))
#用fixture装饰器拿到base_url的地址,上面有详细介绍
@pytest.fixture(scope="function")
def basic_data(base_url):
# print(base_url)
return Module({"base_url": base_url})
#返回Module实例,也就是basic_data = Module类的实例
#执行case,现在先写死,上面介绍的也有关于参数化的方法
def test_queryList(basic_data):
a = basic_data.queryList(2,2,2,3) #basic_data=Module实例
print(a.text)
#断言
assert a.status_code == 200