接口自动化测试-L2

一、接口请求体-文件

1、通过接口上传文件

  • 辨别文件上传接口,查看Content-Type

proxies

image

image

image

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

image

image

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、 使用代理之前

image

2、 使用代理之后

image

3、 代理在接口自动化的使用场景

  • 测试脚本,更直观的排查请求错误,相当于编写代码时的 debug
  • 获取没有错误的,真实的接口请求响应信息
  • 通过代理获取自动化测试的请求响应
  • 对比两次请求响应的区别

4、 Requests 代理

  • 通过 proxies 参数,监听请求与响应信息

image

5、 如何使用代理

    1. 设定代理格式
    1. 通过 proxies 参数传递代理设置
    1. 开启代理工具监听请求
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

  • 获取所有作者
    $..author

  • 获取 store 下面的所有内容
    $.store

  • 获取所有的价格
    $..price

  • 获取第三本书 (从0开始)
    $..book[2]

  • 获取所有包含 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、需求分析

  • 宠物管理业务场景:
    • 添加宠物。
    • 查询宠物信息。
    • 修改宠物信息。
    • 删除宠物。

image

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 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/