一、接口请求体-文件
1、通过接口上传文件
proxies
2、文件上传接口的场景
- 解决接口测试流程中文件上传的问题
- 指定name
- 指定filename
- 指定content-type
3、 使用 requests 上传
import requests
url = "https://httpbin.ceshiren.com/post"
# r = requests.post(url,
# # files参数用来解决文件上传接口
# files={"hogwarts_file": open("1.text", "rb")},
# proxies={"http": "http://127.0.0.1:8080",
# "https": "http://127.0.0.1:8080"},
# verify=False
# )
r = requests.post(url,
# files参数用来解决文件上传接口
# value通过元组传递,实现指定filename
files={"hogwarts_file": ("test.txt", open("1.text", "rb"))},
proxies={"http": "http://127.0.0.1:8080",
"https": "http://127.0.0.1:8080"},
verify=False
)
print(r.json())
二、接口请求体-form表单
1、 FORM 请求
proxies
2、 使用 FORM 请求
import requests
class TestForm:
def setup_class(self):
# 设置代理
self.proxy = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
def test_data(self):
url = "https://httpbin.ceshiren.com/post"
datas = {"a": 1}
# 通过data参数传入请求体信息
r = requests.post(url, data=datas, proxies=self.proxy, verify=False)
print(r.json())
三、接口请求体-xml
1、复杂数据解析
- 数据保存:将复杂的xml或者json请求体保存到文件模板中
- 数据处理:
- 使用mustache、freemaker等工具解析
- 简单的字符串替换
- 使用json、xml、api进行结构化解析
- 数据生成:输出最终结果
四、xml响应断言
1、 XML介绍
- 可扩展标记语言(Extensible Markup Language)的缩写
- 也是一种结构化的数据
2、 XML 断言
from requests_xml import XMLSession
def test_xml():
# 设置session
session = XMLSession()
r = session.get("https://www.nasa.gov/rss/dyn/lg_image_of_the_day.rss")
# 文本格式打印响应内容
print(r.text)
# links可以把响应页面中的所有链接提取出来,并返回一个列表
print(r.xml.links)
# raw_xml返回了字节形式的响应内容
print(r.xml.raw_xml)
# text 返回标签中的内容
print(r.xml.text)
3、 XPath 断言
from requests_xml import XMLSession
def test_xpath():
session = XMLSession()
r = session.get('https://www.nasa.gov/rss/dyn/lg_image_of_the_day.rss')
# 第一个参数为xpath的表达式,first参数表示是否只返回第一个查找的结果,默认False 把所有找到的对象以列表返回,True 表示把查找到的第一个结果返回
item = r.xml.xpath('//link', first=True)
# 拿到Element对象
print(item)
# 调用text属性,拿到link标签下的内容
print(item.text)
# 提取全部,需要进行循环遍历
item1 = r.xml.xpath('//link')
# 定义一个列表,存放提取的内容
result = []
for i in item1:
# 拿到所有link标签的内容,是单个对象,不是列表
print(i.text)
# 返回的内容放在列表中
result.append(i.text)
# 打印列表内容
print(result)
assert "http://www.nasa.gov/" in result
4、 XML 解析
# as ET 起别名
import xml.etree.ElementTree as ET
from requests_xml import XMLSession
def test_xml_tree():
# 设置session
session = XMLSession()
r = session.get("https://www.nasa.gov/rss/dyn/lg_image_of_the_day.rss")
# 自定义封装 xml 解析方法
# 调用fromstring方法,将提取的响应内容放到该方法里,得到整个根元素赋给root
root = ET.fromstring(r.text)
# 调用findall方法,使用xpath表达式,查找根元素
item = root.findall(".")
# 是一个element对象,根元素没有属性,只是遍历子元素的接口
print(item)
# 通过xpath查找子元素,查找link标签下的子元素
items = root.findall(".//link")
result = []
for i in items:
# # 拿到所有link标签的内容
# print(i.text)
# 返回的内容放在列表中
result.append(i.text)
# 打印列表内容
print(result)
assert "http://www.nasa.gov/" in result
五、cookie处理
1、Cookie简介
- Cookie使用场景
- 在接口测试过程中,很多情况下,需要发送的请求附带cookies,才会得到正常的响应的结果。所以使用python+requests进行接口自动化测试也是同理,需要在构造接口测试用例时加入cookie
- 传递Cookie的两种方式
- 通过请求头信息传递
- 通过请求的关键字参数cookies传递
2、自定义header
import requests
class TestReqCookie:
def setup_class(self):
# 设置代理
self.proxy = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
def test_req_header(self):
url = "https://httpbin.ceshiren.com/cookies"
header = {
"Cookie": "hogwarts=school",
"User-Agent": "hogwarts"
} # 注意:Cookie首字母要大写,且没有s
r = requests.get(url=url, headers=header, proxies=self.proxy, verify=False)
print(r.request.headers)
3、使用cookies参数传递
import requests
class TestReqCookie:
def setup_class(self):
# 设置代理
self.proxy = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
def test_req_cookie(self):
self.url = "https://httpbin.ceshiren.com/cookies"
header = {
"User-Agent": "hogwarts"
} # 注意:Cookie首字母要大写,且没有s
# 字典,键值对格式,可以传多条cookie
cookie_data = {
"hogwarts": "school",
"teacher": "AD"
}
r = requests.get(url=self.url, headers=header, cookies=cookie_data, proxies=self.proxy, verify=False)
print(r.request.headers)
六、超时处理
1、 为什么接口测试需要请求超时处理
2、设置超时处理
import requests
class TestReqTime:
def setup_class(self):
# 设置代理
self.proxy = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
def test_001(self):
# 通过timeout参数设置超时时间,参数对应的值通常是一个数字类型
r = requests.post('https://httpbin.ceshiren.com/post', timeout=3, proxies=self.proxy, verify=False)
print(r.json())
def test_002(self):
# 通过timeout参数设置超时时间,参数对应的值通常是一个数字类型
r = requests.post('https://httpbin.ceshiren.com/post', timeout=3, proxies=self.proxy, verify=False)
print(r.json())
def test_003(self):
# 通过timeout参数设置超时时间,参数对应的值通常是一个数字类型
r = requests.post('https://httpbin.ceshiren.com/post', timeout=0.01, proxies=self.proxy, verify=False)
print(r.json())
七、 代理配置
1、 使用代理之前
2、 使用代理之后
3、 代理在接口自动化的使用场景
- 测试脚本,更直观的排查请求错误,相当于编写代码时的 debug
- 获取没有错误的,真实的接口请求响应信息
- 通过代理获取自动化测试的请求响应
- 对比两次请求响应的区别
4、 Requests 代理
5、 如何使用代理
-
- 设定代理格式
-
- 通过 proxies 参数传递代理设置
-
- 开启代理工具监听请求
import requests
class TestProxt:
def setup_class(self):
# 定义一个代理的配置信息的变量,key值为协议,value 为代理工具的配置
self.proxy = {
"http": "http://127.0.0.1:8080",
"https": "http://127.0.0.1:8080"
}
def test_proxy(self):
# 通过proxies 传递代理配置
self.data = {"a": 1}
r = requests.post(url="https://httpbin.ceshiren.com/post", proxies=self.proxy, json=self.data, verify=False)
print(r.json())
八、多层嵌套响应断言
1、 多层嵌套结构
// - 层级多。
// - 嵌套关系复杂。
{
"errcode": 0,
"errmsg": "ok",
"userid": "zhangsan",
"name": "张三",
"department": [1, 2],
"order": [1, 2],
"position": "后台工程师",
"mobile": "13800000000",
"gender": "1",
"email": "zhangsan@gzdev.com",
"biz_mail": "zhangsan@qyycs2.wecom.work",
"is_leader_in_dept": [1, 0],
"direct_leader": ["lisi", "wangwu"],
"avatar": "http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0",
"thumb_avatar": "http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/100",
"telephone": "020-123456",
"alias": "jackzhang",
"address": "广州市海珠区新港中路",
"open_userid": "xxxxxx",
"main_department": 1,
"extattr": {
"attrs": [
{
"type": 0,
"name": "文本名称",
"text": {
"value": "文本"
}
},
{
"type": 1,
"name": "网页名称",
"web": {
"url": "http://www.test.com",
"title": "标题"
}
}
]
},
"status": 1,
"qr_code": "https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx",
"external_position": "产品经理",
"external_profile": {
"external_corp_name": "企业简称",
"wechat_channels": {
"nickname": "视频号名称",
"status": 1
},
"external_attr": [
{
"type": 0,
"name": "文本名称",
"text": {
"value": "文本"
}
},
{
"type": 1,
"name": "网页名称",
"web": {
"url": "http://www.test.com",
"title": "标题"
}
},
{
"type": 2,
"name": "测试app",
"miniprogram": {
"appid": "wx8bd80126147dFAKE",
"pagepath": "/index",
"title": "my miniprogram"
}
}
]
}
}
复杂场景响应提取
场景 方式
提取 errcode 对应的值 res["errcode"]
提取 title 对应的值 res["extattr"]["external_profile"]["external_attr"][1]["web"]["title"]
提取 type 为 0 的 name 编码实现
提取 attrs 下的所有的 name 编码实现
JSONPath 简介
在 JSON 数据中定位和提取特定信息的查询语言。
JSONPath 使用类似于 XPath 的语法,使用路径表达式从 JSON 数据中选择和提取数据。
相比于传统的提取方式,更加灵活,并且支持定制化。
JSONPath 对比
场景 对应实现 JSONPath 实现
提取 errcode 对应的值 res["errcode"] $.errcode
提取 title 对应的值 res["extattr"]["external_profile"]["external_attr"][1]["web"]["title"] 等 $..title
提取 type 为 0 的 name 编码实现 $..external_attr[?(@.type==0)].name
提取 attrs 下的所有的 name 编码实现 $..attrs..name
JSONPath 如何使用
语法知识。
第三方库调用。
JSONPath 语法
符号 描述
$ 查询的根节点对象,用于表示一个 json 数据,可以是数组或对象
@ 过滤器(filter predicate)处理的当前节点对象
* 通配符
. 获取子节点
.. 递归搜索,筛选所有符合条件的节点
?() 过滤器表达式,筛选操作
[start:end] 数组片段,区间为[start,end),不包含 end
[A]或[A,B] 迭代器下标,表示一个或多个数组下标
JSONPath 的练习环境
https://jsonpath.hogwarts.ceshiren.com/
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
},
"expensive": 10
}
JSONPath 练习题目
获取所有书籍的作者
获取所有作者
获取 store 下面的所有内容
获取所有的价格
获取第三本书
获取所有包含 isbn 的书籍
获取所有价格小于 10 的书
获取所有书籍的数量
JSONPath 练习
需求 JsonPath
所有书籍的作者 $.store.book[*].author
所有作者 $..author
store 下面的所有内容 $.store.\*
所有的价格 $.store..price
第三本书 $..book[2]
所有包含 isbn 的书籍 $..book[?(@.isbn)]
所有价格小于 10 的书 $.store.book[?(@.price < 10)]
所有书籍的数量 $..book.length
JSONPath 与代码结合(Python)
环境安装:pip install jsonpath
# 具体的使用。
jsonpath.jsonpath(源数据对象, jsonpath表达式)
JSONPath 与代码结合(Java)
2、 复杂场景响应提取
场景 |
方式 |
提取 errcode 对应的值 |
res[errcode] |
提取 title 对应的值 |
res[extattr][external_profile][external_attr][1][web][title] |
提取 type 为 0 的 name |
编码实现 |
提取 attrs 下的所有的 name |
编码实现 |
3、 JSONPath 简介
- 在 JSON 数据中定位和提取特定信息的查询语言。
- JSONPath 使用类似于 XPath 的语法,使用路径表达式从 JSON 数据中选择和提取数据。
- 相比于传统的提取方式,更加灵活,并且支持定制化。
4、 JSONPath 对比
场景 |
对应实现 |
JSONPath 实现 |
提取 errcode 对应的值 |
res[errcode] |
$.errcode |
提取 title 对应的值 |
res[extattr][external_profile][external_attr][1][web][title] 等 |
$…title |
提取 type 为 0 的 name |
编码实现 |
$…external_attr[?(@.type==0)].name |
提取 attrs 下的所有的 name |
编码实现 |
$…attrs…name |
5、 JSONPath 如何使用
6、 JSONPath 语法
符号 |
描述 |
$ |
查询的根节点对象,用于表示一个 json 数据,可以是数组或对象 |
@ |
过滤器(filter predicate)处理的当前节点对象 |
* |
通配符 |
. |
获取子节点 |
·· |
递归搜索,筛选所有符合条件的节点 |
?() |
过滤器表达式,筛选操作 |
[start:end] |
数组片段,区间为[start,end),不包含 end |
[A]或[A,B] |
迭代器下标,表示一个或多个数组下标 |
7、 JSONPath 练习
- 获取所有书籍的作者
$..book..author
- 获取所有包含 isbn 的书籍
$..book[?(@.isbn)]
- 获取所有价格小于 10 的书
$..book[?(@.price<10)]
- 获取所有书籍的数量(使用length函数)
$..book.length
import requests
import jsonpath
def test_jsonpath():
# httpbin.ceshiren.com是请求什么返回什么
url = "https://httpbin.ceshiren.com/post"
# 定义变量,存放请求体
req_body = {
"name": "holy",
"age": 19
}
# 通过json关键字传递请求体信息
r = requests.post(url, json=req_body)
# print(r.json()["headers"]["Content-Type"])
# jsonpath的返回值,找到会返回结果列表,无论找到几个结果,返回都是列表,没有找到返回False
res = jsonpath.jsonpath(r.json(), "$..Content-Type")
print(res)
九、 宠物商店接口自动化测试实战
1、 实战思路
2、需求分析
- 宠物管理业务场景:
- 添加宠物。
- 查询宠物信息。
- 修改宠物信息。
- 删除宠物。
3、自动化测试脚本思路
自动化代码
"""
完成宠物商城宠物管理功能接口自动化测试。
编写自动化测试脚本。
完成复杂断言。
"""
import requests
class TestPetStore:
def setup_class(self):
# 定义pet_id
self.pet_id = 9223372036854259066
# 定义请求url
self.base_url = "https://petstore.swagger.io/v2/pet"
# 拼接宠物查询接口url
self.search_url = self.base_url + "/findByStatus"
# 删除接口
self.delete_url = self.base_url + f"/{self.pet_id}"
# 宠物状态
self.pet_status = "available"
# 新增宠物信息
self.add_pet_info = {
"id": self.pet_id,
"category": {
"id": 0,
"name": "string"
},
"name": "doggie",
"photoUrls": [
"string"
],
"tags": [
{
"id": 0,
"name": "string"
}
],
"status": self.pet_status
}
# 更新宠物数据
self.update_name = "maomao"
self.update_pet_info = {
"id": self.pet_id,
"category": {
"id": 0,
"name": "string"
},
"name": self.update_name,
"photoUrls": [
"string"
],
"tags": [
{
"id": 0,
"name": "string"
}
],
"status": self.pet_status
}
# 查询宠物数据
self.search_params = {
"status": self.pet_status
}
def test_pet_manager(self):
# 新增宠物接口
add_r = requests.post(self.base_url, json=self.add_pet_info)
assert add_r.status_code == 200
# 查询宠物接口
find_r = requests.get(self.search_url, params=self.search_params)
assert find_r.status_code == 200
# 修改宠物
update_r = requests.put(self.base_url, json=self.update_pet_info)
assert update_r.status_code == 200
# 删除宠物
delete_r = requests.delete(self.delete_url)
assert delete_r.status_code == 200
代码优化
- 配置代理查看接口数据
- 添加日志
- 使用 jsonpath 断言
日志配置:log_utils.py
# 配置日志
import logging
import os
from logging.handlers import RotatingFileHandler
# 绑定绑定句柄到logger对象
logger = logging.getLogger(__name__)
# 获取当前工具文件所在的路径
root_path = os.path.dirname(os.path.abspath(__file__))
# 拼接当前要输出日志的路径
log_dir_path = os.sep.join([root_path, '..', f'/logs'])
if not os.path.isdir(log_dir_path):
os.mkdir(log_dir_path)
# 创建日志记录器,指明日志保存路径,每个日志的大小,保存日志的上限
file_log_handler = RotatingFileHandler(os.sep.join([log_dir_path, 'log.log']), maxBytes=1024 * 1024, backupCount=10)
# 设置日志的格式
date_string = '%Y-%m-%d %H:%M:%S'
formatter = logging.Formatter(
'[%(asctime)s] [%(levelname)s] [%(filename)s]/[line: %(lineno)d]/[%(funcName)s] %(message)s ', date_string)
# 日志输出到控制台的句柄
stream_handler = logging.StreamHandler()
# 将日志记录器指定日志的格式
file_log_handler.setFormatter(formatter)
stream_handler.setFormatter(formatter)
# 为全局的日志工具对象添加日志记录器
# 绑定绑定句柄到logger对象
logger.addHandler(stream_handler)
logger.addHandler(file_log_handler)
# 设置日志输出级别
logger.setLevel(level=logging.INFO)
优化后完整代码
"""
完成宠物商城宠物管理功能接口自动化测试。
编写自动化测试脚本。
完成复杂断言。
"""
import allure
import jsonpath
import requests
from interface.utils.log_utils import logger
@allure.feature("宠物管理业务场景接口测试")
class TestPetStore:
def setup_class(self):
# 代码优化,配置代理查看接口数据
self.proxy = {
"http": "http://127.0.0.1:8888",
"https": "http://127.0.0.1:8888"
}
# 定义pet_id
self.pet_id = 9223372036854259969
# 定义请求url
self.base_url = "https://petstore.swagger.io/v2/pet"
# 拼接宠物查询接口url
self.search_url = self.base_url + "/findByStatus"
# 删除接口
self.delete_url = self.base_url + f"/{self.pet_id}"
# 宠物状态
self.pet_status = "available"
# 新增宠物信息
self.add_pet_info = {
"id": self.pet_id,
"category": {
"id": 0,
"name": "string"
},
"name": "doggie",
"photoUrls": [
"string"
],
"tags": [
{
"id": 0,
"name": "string"
}
],
"status": self.pet_status
}
# 更新宠物数据
self.update_name = "maomao"
self.update_pet_info = {
"id": self.pet_id,
"category": {
"id": 0,
"name": "string"
},
"name": self.update_name,
"photoUrls": [
"string"
],
"tags": [
{
"id": 0,
"name": "string"
}
],
"status": self.pet_status
}
# 查询宠物数据
self.search_params = {
"status": self.pet_status
}
@allure.story("宠物的增删改查场景测试")
def test_pet_manager(self):
# with allure.step添加步骤
with allure.step("新增宠物"):
# 新增宠物接口
add_r = requests.post(self.base_url, json=self.add_pet_info, proxies=self.proxy, verify=False)
# 添加日志
logger.info(f"新增宠物接口的响应为:{add_r.text}")
# 断言
assert add_r.status_code == 200
with allure.step("查询宠物"):
# 查询宠物接口
search_r = requests.get(self.search_url, params=self.search_params, proxies=self.proxy, verify=False)
# 添加日志
logger.info(f"查询宠物接口的响应为:{search_r.text}")
# 断言
assert search_r.status_code == 200
# 新增宠物业务断言,查询接口中是否包含新增宠物的id
# 使用jsonpath提取响应内容,find_r.json()查询结果转换为json对象
search_js = jsonpath.jsonpath(search_r.json(), "$..id")
assert self.pet_id in search_js
with allure.step("更新宠物"):
# 更新宠物
update_r = requests.put(self.base_url, json=self.update_pet_info, proxies=self.proxy, verify=False)
# 添加日志
logger.info(f"更新宠物接口的响应为:{update_r.text}")
# 断言
assert update_r.status_code == 200
# 再次查询宠物
search_r1 = requests.get(self.search_url, params=self.search_params, proxies=self.proxy, verify=False)
# 更新宠物的业务断言,查询接口中是否包含更新宠物的name
update_js = jsonpath.jsonpath(search_r1.json(), "$..name")
assert self.update_name in update_js
with allure.step("删除宠物"):
# 删除宠物
delete_r = requests.delete(self.delete_url, proxies=self.proxy, verify=False)
# 添加日志
logger.info(f"删除宠物接口的响应为:{delete_r.text}")
# 断言
assert delete_r.status_code == 200
# 再次查询宠物
search_r2 = requests.get(self.search_url, params=self.search_params, proxies=self.proxy, verify=False)
# 业务断言,查询接口中不包含宠物的id
delete_js = jsonpath.jsonpath(search_r2.json(), "$..id")
assert self.pet_id not in delete_js
生成测试报告
# 生成报告信息
pytest -vs test_petsearch_l2.py --alluredir=./reports/
# 生成报告在线服务,查看报告
allure serve ./reports/