可直接落地的pytest+request+allure接口自动化框架
一、框架结构简介
框架采用python3+pytest+request+allure搭建,需要有一定代码基础方可读懂,代码内每个方法都加有注释方便理解,该框架相对比较完善,可直接落地公司项目进行使用,也可根据公司项目情况继续完善开发。
框架目录结构:
- common 用于存放公共方法的包,比较核心
- config 用于存放配置文件
- img 用于存放图片文件
- report 用于存放测试报告
- testcases 用于存放测试用例
- token_dir 用于存放接口所使用的token,避免重复调用登录接口
二、config_yaml配置文件
为了框架使用起来更加方便,更加便捷,更加好维护,这里我们使用yaml来把一些需要经常修改的内容放到里面
我这里目前存放了接口的账号密码、测试环境域名、数据库,有其他需要可以继续进行添加补充
文件内容结构如下:
# key可以照着这个来,value都是假的(*^▽^*),根据自己项目来
# 内容暂时这么多,格式需要固定这个,方便后面读取,具体怎么用下面会讲到
user:
york:
username: 166xxxxxxxx
password: xxxxxxxxxxxxxxxx
laoyan:
username: 188xxxxxxxx
password: xxxxxxxxxxxxxxxx
test_url: https://www.baidu.com
mysql:
db: test_system
host: 666.888.999.555
password: 123456
port: 3306
user: root
三、common包公共方法
common包方法简介:
commom_requests.py
用于接口请求, 核心方法deal_with_response.py
处理allure测试报告,为报告增加附件信息login.py
于处理登录接口,储存tokenmysql_operate.py
处理数据库,操作数据库增改查tools.py
处理文件路径,拼接文件路径获取项目路径yaml_config.py
读取yaml配置文件
3.1 tools.py 代码讲解
该文件为获取当前项目路径,以及封装拼接接口路径等方法(实现方式不唯一,可以适当调整)
因该文件内方法最为基础,后面都要用到,所以第一步优先编写
代码如下:
# 导入os包
import os
def get_project_path():
"""
获取项目目录
:return:
"""
# api_auto_test为项目名称,可以自行调整
project_name = "api_auto_test"
# 获取当前项目路径
file_path = os.path.dirname(__file__)
# 因为file_path返回的是当前文件所在位置的目录,而我们需要项目的跟目录
# 所以这里使用切片,把返回的路径切片到刚好为根目录的地方(方法不唯一)
a = file_path[:file_path.find(project_name) + len(project_name)]
return a
def sep(path, add_sep_before=False, add_sep_after=False):
"""
拼接文件路径,添加系统分隔符
:param path: 路径列表,类型为数组 ["config","environment.yaml"]
:param add_sep_before: 是否需要在拼接的路径前加一个分隔符
:param add_sep_after: 是否需要再拼接的路径后加一个分隔符
:return:
"""
# 拼接传入的数组
all_path = os.sep.join(path)
# 如果before为TRUE,那就在路径前面加“/”
if add_sep_before:
all_path = os.sep + all_path
# 如果after为TRUE,那就在路径后面加“/”
if add_sep_after:
all_path = all_path + os.sep
return all_path
if __name__ == '__main__':
# 测试一下
print(get_project_path())
print(sep(["config","environment.yaml"], add_sep_before=True))
测试代码结果如下:
很成功,可以看到控制台成功打印出了项目路径,以及使用拼接方法成功拼接了输入的路径,并带有分隔符
3.2 yaml_config.py 代码讲解
处理完获取路径的方法后,那么紧接着就是要获取配置文件了,配置文件内容在上方第二段已经给出;
那么下面我们根据yaml内容,来编写读取的方法。
代码如下:
# 导出处理yaml文件的包
import yaml
# 导入3.1编写好的tools里的方法
from common.tools import get_project_path, sep
class GetConfig:
# 使用构造函数,初始化yaml文件,把yaml文件读取出来
def __init__(self):
# 用tools里的get_project_path()获取项目路径
project_path = get_project_path()
# 使用with——open方法读取yaml文件内容
# open里的project_path + sep(["config", "environment.yaml")用于把yaml文件路径拼出来
with open(project_path + sep(["config", "environment.yaml"], add_sep_before=True), "r",
encoding="utf-8") as env_file:
# 使用yaml.load方法把读取出的文件转化为列表或字典,方便后续取值
# Loader=yaml.FullLoader意思为加载完整的YAML语言,避免任意代码执行
self.env = yaml.load(env_file, Loader=yaml.FullLoader)
def get_username_password(self, user):
"""
读取配置文件里的账号密码
:param user: 需要取哪一个账号的就输入对应的名称,比如我想去york的账密,user就传“york”
:return:
"""
# 直接return出来对应的账号密码
return self.env["user"][f"{user}"]["username"], self.env["user"][f"{user}"]["password"]
def get_url(self):
"""
测试地址
:return:
"""
# 直接return出来对应的测试域名
return self.env["url"]
def get_mysql_config(self):
"""
获取数据库配置
:return:
"""
# 直接return出来对应yaml里的数据库参数,输出字典
return self.env["mysql"]
# 测试一下
if __name__ == "__main__":
getConfig = GetConfig()
print(getConfig.get_username_password("york"))
print(getConfig.get_url())
print(getConfig.get_mysql_config())
输出结果如下:
可以看到想要拿到的东西都读取到了,后面用起来直接调用对应方法即可,方便又快捷,性价比极高。
3.3 deal_with_response.py 代码讲解
因下面在封装request的时候,需要用到对测试报告的处理,所以这里优先讲一下这里;
该文件主要是用来把接口请求的一些必要的信息,显示到allure报告里,查看报告的时候更容易查看接口的具体情况,可以看下加上这段代码的报告下过,如下图所示:
可以看到报告内圈住地方有七行信息,分别对应下面我们要写的七个方法;
代码如下:
# 导入allure
import allure
def deal_with_res(data, res):
# 主要用到了allure.attach,在接口请求时可以把必要的信息存放到报告里查看
# 一一把需要显示的内容获取到,然后使用attach存放到报告
# 方法里的res就是后面接口请求的内容,data就算是入参报文
# 请求的url
request_url = str(res.request.url)
allure.attach(request_url, "请求的url")
# 请求的方法
request_method = str(res.request.method)
allure.attach(request_method, "请求的方法")
# 请求的headers
request_headers = str(res.request.headers)
allure.attach(request_headers, "请求的headers")
# 入参报文
request_data = str(data)
allure.attach(request_data, "入参报文")
# 响应时间
response_time = str(res.elapsed.total_seconds() * 1000)
allure.attach(response_time, "响应时间")
# 状态码
status_code = str(res.status_code)
allure.attach(status_code, "状态码")
# 响应报文
response_text = str(res.text)
allure.attach(response_text, "响应报文")
3.4 commom_requests.py 代码讲解
下面到了整个框架最为核心的代码了,这个文件主要用于封装requests接口请求的方法;
目前我只封装了post和get方法,像put、delete等用的比较少,暂时没写,可自行添加;
框架主要用到了requests和adapters,前者不用多说,后者是用来处理当接口请求失败了,可自动重试,具体的可以看下源码或者网上的讲解,不具体讲了。
代码如下:
# 导入requests
import requests
# 导入adapters,处理接口重试
from requests.adapters, import HTTPAdapter
# 导入前面写的两个方法
from common.yaml_config import GetConfig
from common.deal_with_response import deal_with_res
class Requests:
# 构造函数,初始化session,封装requests
def __init__(self, headers=None, timeout=None):
"""
封装requests方法
:param headers:接口的header
:param timeout:如果需要设置设置超时时间就传,默认None
"""
self.s = requests.Session()
# 在session实例上挂载adapter实例,目的就是请求异常时,自动重试
self.s.mount("http://", HTTPAdapter(max_retries=3))
self.s.mount("https://", HTTPAdapter(max_retries=3))
# 公共请求头设置,把对应的值设置好
self.s.headers = headers
self.timeout = timeout
# 调用获取yaml里的url,把测试域名拿出来,下面做拼接接口用
self.url = GetConfig().get_url()
def get_request(self, url, params=None):
"""
GET方法封装
:param url: 接口地址
:param params: 一般GET的参数都是放在URL里面
:return:
"""
# 可以看到用yaml里的self.url加上接口路径,就是完整的接口了
# 后面要测试uat或者生产环境直接的话直接改yaml里面的域名就好了
res = self.s.get(self.url + url, params=params, timeout=self.timeout)
# 调用处理报文的方法,把接口信息加入到测试报告
deal_with_res(params, res)
return res
def post_request(self, url, data=None, json=None):
"""
POST方法封装
:param url: 接口地址
:param data: 参数放在表单中
:param json: 参数放在请求体中,一般是json
:param headers:
:return:
"""
# 如果传入的是表单,那接口就传data,适用一些接口是form-data格式的
if data:
res = self.s.post(self.url + url, data=data, timeout=self.timeout)
# 调用处理报文的方法,把接口信息加入到测试报告
deal_with_res(data, res)
return res
# 如果传入的json,就传入json,适用大部分接口
if json:
res = self.s.post(self.url + url, json=json, timeout=self.timeout)
# 调用处理报文的方法,把接口信息加入到测试报告
deal_with_res(json, res)
return res
# 有些post接口是什么也不传的,兼容这种情况
res = self.s.post(self.url + url, timeout=self.timeout)
# 调用处理报文的方法,把接口信息加入到测试报告
deal_with_res(json, res)
return res
# 魔法函数
def __del__(self):
"""
当实例被销毁时,释放掉session所持有的连接
:return:
"""
if self.s:
self.s.close()
# 测试一下下
if __name__ == '__main__':
# 这里域名设置的是http://httpbin.org,懂得都懂
get_res = Requests().get_request("/get")
post_res = Requests().post_request("/post")
print(get_res.text, "\n", post_res.text, "\n")
执行结果:
这里测试用的是http://httpbin.org,这个网站提供了各种各样的测试接口,用来练习很不错
可以看到,都通了,没毛病
3.5 mysql_operate.py 代码讲解
在日常工作中,有很多场景,需要接口请求后与数据库里的数据做对比。
因此我们这里直接封装一套兼并数据查、改、增的方法,为什么没有删呢,因为删库容易出事ヽ(ー_ー)ノ,而且很多公司都不会给测试放删库的权限,dddd(懂得都懂),因此就不加删的方法了,需要的话可以自行添加。
下面上代码:
"""
封装数据库的增改查方法
"""
# 导入处理数据库的包
import pymysql
# 老样子,导入获取yaml的方法来读取数据库配置信息
from common.yaml_config import GetConfig
class MysqlOperate:
# 初始化数据库,把数据库字段映射上
def __init__(self):
# 获取到yaml里的数据库配置信息
mysql_config = GetConfig().get_mysql_config()
# 把获取的值一一对应上
self.host = mysql_config['host']
self.db = mysql_config['db']
self.port = mysql_config['port']
self.user = mysql_config['user']
self.password = mysql_config['password']
self.conn = None
self.cursor = None
# 数据库建立连接
def __conn_db(self):
# 用到try,因为有时候会出现数据库连接失败的情况,
try:
self.conn = pymysql.connect(
host=self.host,
user=self.user,
password=self.password,
db=self.db,
port=self.port,
charset='utf8'
)
except Exception as e:
print(e)
return False
self.cur = self.conn.cursor()
return True
# 关闭数据库连接,随手关门养成好习惯
def __close_conn(self):
self.cur.close()
self.conn.close()
return True
# 增、改后要commit一下,提交到数据库
def __commit(self):
self.conn.commit()
return True
def query(self, sql):
"""
查询数据库
:param sql: sql查询语句
:return:
"""
# 建立连接
self.__conn_db()
# 操作数据库查询
self.cur.execute(sql)
query_data = self.cur.fetchall()
# 如果查询的是空,就返回None
if query_data == ():
query_data = None
print("没有获取到数据")
# 不是空就继续往下走
else:
pass
# 关闭数据库连接
self.__close_conn()
# return出查询的接口
return query_data
def insert_update_table(self, sql):
"""
插入数据或者修改数据
:param sql: 增、改sql语句
:return:
"""
# 建立连接
self.__conn_db()
# 执行sql
self.cur.execute(sql)
# commit一下
self.__commit()
# 关闭数据库连接
self.__close_conn()
# return出查询的接口
return True
这里就不做测试了,大家可以用自己公司的测试库测试一下,我试过了,没毛病的(^_−)☆。
3.6 login.py 代码讲解
这个方法比较简单,用意也很简单,就是因为登录接口很多地方都会依赖,所以干脆封装起来,后面用着方便。
如果有一些其他的,调用频繁的接口,也可以放到这个文件里。
上代码:
# 导入获取yaml方法
from common.yaml_config import GetConfig
# 导入封装好的request
from common.commom_requests import Requests
def login(user):
"""
封装登录接口
:param user: yaml文件里账号密码的用户名称
:return:
"""
# 取出账号密码
username, password = GetConfig().get_username_password(user)
# 赋值给登录接口的入参
login_data = {
"name": f"{username}",
"pwd": f"{password}"
}
# 执行接口请求
login_res = Requests().post_request("/login", data=login_data)
# 返回出参
return login_res
# 测试一下,道友们可以用自己公司系统测试
if __name__ == '__main__':
print(login("york").json())
执行结果如下:
非常完美,后面再用到登录直接掉就完事了
3.7 总结
好的,到这里我们的整个common包就完成编写了,整个common是我们这套框架的发动机,下面的测试案例编写都会用到这些,具体的用法,下面我会编写一些样例测试case,可供参考;
目前这些方法是我能想到的了,后面如果大家有补充,也可以自行添加。
四、接口测试案例
我们的测试案例使用的是pytest测试框架,为什么要用pytest呢?
pytest是一个非常成熟的全功能的Python测试框架,主要有以下几个特点:
- 简单灵活,容易上手;
- 支持参数化;
- 能够支持简单的单元测试和复杂的功能测试,还可以用来做selenium/appnium等自动化测试、接口自动化测试(pytest+requests);
- pytest具有很多第三方插件,并且可以自定义扩展,比较好用的如pytest-selenium(集成selenium)、pytest-html(完美html测试报告生成)、pytest-rerunfailures(失败case重复执行)、pytest-xdist(多CPU分发)等;
- 测试用例的skip和xfail处理;
- 可以很好的和jenkins集成;
- report框架----allure 也支持了pytest;
pytest的用法及其丰富,后面我会单独给出介绍,这里就不细说了,大家也可以自行翻阅网上的资料
4.1 pytest框架之conftest.py&fixture夹具
这里我要单独介绍一下pytest
的conftest
,他为后续的测试提供很大的编写,一些公用的前置或后置测试案例都可以放到这里面,pytest
会自动读取引用,配合fixture
简直是好用到爆
conftest的特性
- conftest是可以跨文件使用的
- conftest.py这个文件名是固定的,不能更改
- 就近原则 如果同级目录有,就引用同级目录的conftest文件,如果没有就向上级查找
- conftest不能被其他文件导入
- conftest可以设置多个pytest内置的钩子函数
4.2 conftest代码编写
在日常做测试工作中,有很多接口是依赖登录接口返回的token的。
那如果每个案例都都调用一下登录接口的话,那代码也太冗余了。
如果测试用例数量很庞大的话,一般都会用到pytest-xdist插件,来分布式执行测试案例,那这时候对登录接口会造成一定的压力。
所以这里封装一个存储token的方法,存放到夹具里,逻辑是先判断存储token的文件是否存在,如果存在就直接取用token,如果不存在那就调用登录接口,取出token,生成文件,存放到文件里。
后面每个案例执行的时候,都会按上述逻辑走一遍,有token就直接取,没token再调登录接口,并发拿到的token存起来,其他案例就直接取。
conftest文件需要存放在testcases文件夹里,后续测试案例也需要放在这个文件夹里。
上代码:
# 导入各种包,不一一介绍了,下面代码都要有用到的
import os
import json
import pytest
# 读取数据库的方法也可加到夹具里,我没加
from common.mysql_operate import MysqlOperate
from common.login import login
from common.tools import sep, get_project_path
# pytest的精髓,夹具fixture,效果类似setup
@pytest.fixture()
def token():
def _token(user):
# 判断存放token文件的文件夹是否存在,不存在则自动创建
token_json_dir = sep([get_project_path(), "token_dir"])
if not os.path.exists(token_json_dir):
os.mkdir(token_json_dir)
# 生成用户user对应token的json文件
token_json_path = sep([token_json_dir, user + "_token.json"])
# 若文件不存在,调用登录接口,并把token写入json文件
if not os.path.exists(token_json_path):
print(f"{user}对应的token的json文件不存在,调用登录接口")
# 调用登录方法,拿到token,每个系统的token字段名不一样,自行修改
token = login(user).json()["data"]
print(f"写入{user}对应token的json文件{token}")
# 拿到token后,开始生成token文件,并写入token
with open(token_json_path,"w+") as write_token:
# 写入是时候是键值对的形式,方便拿取
write_token.write(json.dumps({"token": token}))
# return出token
return token
else:
# 文件存在了,直接取出文件里面的token
print(f"{user}对应的token_json文件存在,直接取文件token")
with open(token_json_path, "r") as token_info:
token = json.loads(token_info.read())
# 因为token是键值对的形式,需要取一下
return token["token"]
return _token
这里就不做测试了,下面附上conftest的工作流程,后面写测试案例的时候会用到这一块的代码。
4.3 测试案例编写
测试案例我们放到testcases文件夹里,需要与conftest在一个文件夹。
这里我拿我们公司的新增用户的接口进行测试,为了保证公司隐私,接口部分我会写成假的,各位自行修改。
# 导入必要的包
import pytest
from common.commom_requests import Requests
# 测试案例必须要以Test开头
class TestAddUser:
# 敲重点,test_add_user(self, token),看到没,这里的token就是conftest里面的token方法
# 在写测试案例的时候,如果要用到conftest里面的方法,就直接把方法名带上
# 代码下方我会付上它的执行流程图
def test_add_user(self, token):
# header里需要带入token,这里直接
header = {
"Access-Token": token("york")
}
# 接口入参,
json = {
"phone": "13288889999",
"title": "测试一下",
"userName": "测试一下嘿嘿嘿",
}
# 调用封装好的request,传入headers。
# 调用post方法,传入接口路径,方法会自动拼接域名;传入json入参。
res = Requests(headers=header).post_request("/user/add", json=user_data)
# 打印一下方便查看
print(res.json())
# 做一个简单的断言
assert res.status_code == 200
执行一下,第一次执行,下面为测试结果:
注意,第一次执行是没有存放token文件的文件夹的,看到没,圈住的,案例自动走了conftest里面的方法,
因为是第一次执行,没有文件夹,就自动创建了文件夹,没有json文件就自动创建了文件。
是不是很酷!!
生成的文件夹以及文件截图:
现在已经有现成的token了,不需要再调用登录了,那么我们再执行一次案例,看看效果:
ok,非常酷,逻辑没问题,发现有token,直接拿了token,去执行案例,接口执行成功。
附上conftest的流程图:
4.4 总结
测试案例基本上千篇一律,可以仿照上面的列子自行添加测试案例。
添加测试案例又是一门很深的学问,可以用到各种pytest自带的类似夹具的方法。
这里就不一一介绍了,网上资料多得很,可以自己查一下。
五、Allure测试报告
5.1 安装allure
- 到github上的allure2项目下载zip包,挑一个版本下载,在版本中的Assets里找到zip包下载
地址:https://github.com/allure-framework/allure2/releases - 把下载的zip包解压缩到python目录的Lib\site-packages,比如windows上在环境变量加上类似这种C:\python3\Lib\site-packages\allure-2.10.0\bin
- 在环境变量的path中,把allure的bin目录添加上去
终端执行 allure --version 检查是否出现版本号,出现则为安装成功 - 以上安装的是allure的服务端,还要安装一下allure提供的python包,使用
pip install allure-pytest
5.2 使用allure
终端执行命令:
pytest -s testcases\test_add_user.py --alluredir=report
--alluredir=report
生成测试报告的目录
根据执行结果,生成测试报告,查看allure报告:
allure generate report -o report/api_report
意思是在report文件下,用执行完用例的结果,生成测试报告,报告存放在api_report文件夹下面
执行结果:
5.3 allure注释功能
5.4使用allure生成测试报告
这里我们拿上面的新增用户的案例代码,加入allure的注释代码,来做个例子:
import pytest
import allure
from common.commom_requests import Requests
json = {
"phone": "13288889999",
"remark": "测试一下",
"userName": "测试一下嘿嘿嘿",
}
@allure.description("调用企业管理添加用户接口")
@allure.epic("企业管理")
@allure.feature("用户管理")
@allure.story("添加用户")
@allure.tag("新增")
class TestAddUser:
@pytest.mark.parametrize("user_data", user_data)
def test_add_user(self, token, user_data):
with allure.step("登录获取tolen"):
header = {
"Access-Token": token("york")
}
with allure.step("调用新增用户接口"):
res = Requests(headers=header).post_request("/user/add", json=user_data)
print(res.json())
with allure.step("断言"):
assert res.status_code == 200
执行后生成测试报告如下,一一对应报告,如图所示:
是不是这么一搞,报告美观很多。
六、框架总结
没有总结,你就拿去用吧,一用一个不吱声(o゚▽゚)o !
纯手打,转载注明出处,Thanks♪(・ω・)ノ!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?