软件测试 / 测试开发丨学习笔记之接口自动化测试
- 2023-05-29 北京
本文字数:9973 字
阅读完需:约 33 分钟
获取更多相关知识
本文为霍格沃兹测试开发学社学员学习笔记分享,文末附原文链接
一、接口请求体-文件
1、通过接口上传文件
辨别文件上传接口,查看 Content-Type
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 ETfrom 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 代理
通过 proxies 参数,监听请求与响应信息
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..nameJSONPath 如何使用语法知识。第三方库调用。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所有作者 $..authorstore 下面的所有内容 $.store.*所有的价格 $.store..price第三本书 $..book[2]所有包含 isbn 的书籍 $..book[?(@.isbn)]所有价格小于 10 的书 $.store.book[?(@.price < 10)]所有书籍的数量 $..book.lengthJSONPath 与代码结合(Python)环境安装:pip install jsonpath# 具体的使用。jsonpath.jsonpath(源数据对象, jsonpath表达式)JSONPath 与代码结合(Java)
2、 复杂场景响应提取
3、 JSONPath 简介
在 JSON 数据中定位和提取特定信息的查询语言。
JSONPath 使用类似于 XPath 的语法,使用路径表达式从 JSON 数据中选择和提取数据。
相比于传统的提取方式,更加灵活,并且支持定制化。
4、 JSONPath 对比
5、 JSONPath 如何使用
语法知识。
第三方库调用。
6、 JSONPath 语法
7、 JSONPath 练习
获取所有书籍的作者
$..book..author
获取所有作者
$..author
获取 store 下面的所有内容
$.store
获取所有的价格
$..price
获取第三本书 (从 0 开始)
$..book[2]
获取所有包含 isbn 的书籍
$..book[?(@.isbn)]
获取所有价格小于 10 的书
$..book[?(@.price<10)]
获取所有书籍的数量(使用 length 函数)
$..book.length
九、 宠物商店接口自动化测试实战
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 断言使用 jsonpath 实现多层嵌套响应的断言。
日志配置:log_utils.py
# 配置日志import loggingimport 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 allureimport jsonpathimport requestsfrom 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/ 版权声明: 本文为 InfoQ 作者【测试人】的原创文章。
原文链接:【http://xie.infoq.cn/article/46f186eddfe889b8ee36f56b3】。文章转载请联系作者。
测试人
专注于软件测试开发 2022-08-29 加入
霍格沃兹测试开发学社,测试人社区:https://ceshiren.com/t/topic/22284










评论