写点什么

软件测试 / 测试开发丨接口测试实战学习笔记

作者:测试人
  • 2023-06-01
    北京
  • 本文字数:9052 字

    阅读完需:约 30 分钟

获取更多相关知识

本文为霍格沃兹测试开发学社学员学习笔记分享,文末附原文链接。

一、接口加密与解密

1、 环境准备

  • 对响应加密的接口。对它发起一个 get 请求后,得到一个加密过后的响应信息。(如果有可用的加密过的接口以及了解它的解密方法,可以跳过)

  • 准备一个加密文件

  • 使用 python 命令在有加密文件的所在目录启动一个服务

  • 访问该网站



2、 原理

  • 在得到响应后对响应做解密处理:如果知道使用的是哪个通用加密算法的话,可以自行解决。如果不了解对应的加密算法的话,可以让研发提供加解密的 lib。如果既不是通用加密算法、研发也无法提供加解密的 lib 的话,可以让加密方提供远程解析服务,这样算法仍然是保密的。

"""调用python自带的base64,直接对返回的响应做解密,即可得到解密后的响应。封装对于不同算法的处理方法。"""import requestsimport base64import json

def test_encode(): url = "http://127.0.0.1:9999/demo.txt" r = requests.get(url) print(r.text) # content 获取一个二进制的响应结果,对加密内容进行解密,使用json.loads()优化格式 res = json.loads(base64.b64decode(r.content)) print(res)
复制代码

封装对于不同算法的处理方法

"""调用python自带的base64,直接对返回的响应做解密,即可得到解密后的响应。封装对于不同算法的处理方法。"""import requestsimport base64import json

class ApiRequest: def send(self, data:dict): res = requests.request(data["method"], data["url"], headers=data["headers"]) if data["encoding"] == "base64": return json.loads(base64.b64decode(res.content))
# 把加密过后的响应值发给第三方服务,让第三方去做解密然后返回解密过后的信息,private是自定义的,不是固定 elif data["encoding"] == "private": return requests.post("url", data=res.content)
复制代码

测试封装的方法,新建 py 文件

from interface import test_api_request
class TestApiRequest: res_data = { "method": "get", "url": "http://127.0.0.1:9999/demo.txt", "headers": None, "encoding": "base64" }
def test_send(self): # 实例化对象 ar = test_api_request.ApiRequest() print(ar.send(self.res_data))
复制代码

二、 多套被测环境

1、 多环境介绍


2、 多套被测环境切换的意义和价值

  • 访问信息: 不同环境的域名或 ip 都不一样,部分产品 Host 也会有区别

  • 配置信息: DB、Redis、ES 等中间件的配置信息不同环境也不一样

  • 每条用例的 url 都是写死的,一旦切换环境,所有的用例都要修改。

3、 实现目标

  • 全局控制,一键切换

  • 可维护性和扩展性强,可以应对不断演进的环境变化。环境管理环境切换

4、 环境管理

  • 使用环境管理文件 yamlini 常量类

  • 使用不同的文件管理不同的环境

  • 在接口用例中只指定 path,不指定 url

5、环境切换

  • 通过环境变量进行切换

  • 通过命令行参数进行切换

6、 通过环境变量进行切换

  • 设置环境变量

  • 读取环境变量

# mac设置环境变量,尽量不直接使用env,env可能会和底层环境变量重名,可以使用interface_env等export interface_env=dev# windows 设置环境变量set interface_env=dev
复制代码

新建 test.yaml 文件

# 测试环境的配置文件base_url: https://httpbin.org/
复制代码

新建 dev.yaml 文件

# 开发环境的配置文件base_url: https://httpbin.ceshiren.com/
复制代码

代码

import osimport requestsimport yaml

# 设置临时环境变量# Mac: export interface_env="test"# windows:set interface_env="test"# window如果使用的是powershell,需要使用$env:interface_env="test" 命令设置环境变量class TestMulitiEnv: def setup_class(self): # 在接口用例中只指定path,不指定url # 从yaml文件读取数据,env读取出来是一个字典 # 优化点:第一种方式:从环境变量读取名称为inter_env的配置环境,default给默认值 path_env = os.getenv("inter_env", default="test") env = yaml.safe_load(open(f"{path_env}.yaml", encoding="utf-8")) # env = yaml.safe_load(open("dev.yaml", encoding="utf-8")) self.base_url = env["base_url"]
def test_devenv(self): """ 验证是否为开发环境 :return: """ path = "get" r = requests.get(self.base_url + path) # print(r.json()) # 假设httpbin.ceshiren.com是开发环境,则断言当前请求是否是向"开发环境"发起的 assert r.json()["headers"]["Host"] == "httpbin.ceshiren.com"
def test_testenv(self): """ 验证是否为测试环境 :return: """ path = "get" r = requests.get(self.base_url + path) # r = requests.get("https://httpbin.org/get") assert r.json()["headers"]["Host"] == "httpbin.org"
复制代码

终端执行用例

# 设置开发环境变量export interface_env="dev"pytest test_muliti_env.py 
# 设置测试环境变量export interface_env="test"pytest test_muliti_env.py
复制代码

7、 使用命令行进行切换


conftest.py 文件

# 声明变量 global_envglobal_env = {}

# hook 函数,是添加命令行参数使用def pytest_addoption(parser): # group 将下面所有的 option都展示在这个group下。 mygroup = parser.getgroup("hogwarts") # 注册一个命令行选项 mygroup.addoption("--env", # 参数的默认值 default='test', # 存储的变量 dest='env', # 参数的描述信息 help='设置接口自动化测试默认的环境' )

# 获取设置对命令行参数,并传递给一个变量global_envdef pytest_configure(config): default_ev = config.getoption("--env") tmp = {"env": default_ev} global_env.update(tmp)
复制代码

代码

import requestsimport yamlfrom interface.conftest import global_env

class TestMulitiEnvByOption: def setup_class(self): path_env = global_env.get("env") env = yaml.safe_load(open(f"{path_env}.yaml", encoding="utf-8")) self.base_url = env["base_url"]
def test_devenv(self): """ 验证是否为开发环境 :return: """ path = "get" r = requests.get(self.base_url + path) # print(r.json()) # 假设httpbin.ceshiren.com是开发环境,则断言当前请求是否是向"开发环境"发起的 assert r.json()["headers"]["Host"] == "httpbin.ceshiren.com"
def test_testenv(self): """ 验证是否为测试环境 :return: """ path = "get" r = requests.get(self.base_url + path) # r = requests.get("https://httpbin.org/get") assert r.json()["headers"]["Host"] == "httpbin.org"
复制代码

终端执行

pytest test_muliti_by_option_env.py --env=testpytest test_muliti_by_option_env.py --env=dev
复制代码

三、多响应类型封装设计

1、 多协议封装应用场景

  • 问题:响应值不统一 jsonxml 断言比较困难

  • 解决方案:获得的响应信息全部转换为结构化的数据进行处理

图一:未优化前


import requestsimport xmltodict

def test_xml_to_dict(): res = requests.get("https://www.nasa.gov/rss/dyn/lg_image_of_the_day.rss") # 注意,打印res的texts属性 print(res.text) # 转换为python的dict格式 dict_res = xmltodict.parse(res.text) print(dict_res)
复制代码

图二:优化后的


"""多协议封装:对响应值做二次封装,可以使用统一提取方式完成断言"""import requestsimport xmltodictfrom requests import Response

def test_response_to_dict(): # res = requests.get("https://www.nasa.gov/rss/dyn/lg_image_of_the_day.rss") res = requests.get("https://httpbin.ceshiren.com/get") final_res = response_to_dict(res) # 断言响应值是否为dict类型的格式 assert isinstance(final_res, dict)

def response_to_dict(response: Response): # 获取text属性 res_text = response.text # 判断响应文本信息是否以 <?xml 开头 if res_text.startswith("<?xml"): final_dict = xmltodict.parse(res_text) elif res_text.startswith("<!DOCTYPE html>"): final_dict = "html" # 如果是json 则返回 json 格式 else: final_dict = response.json() return final_dict
复制代码

四、电子商城接口自动化测试实战 L4

1、问题

  • 可维护性差:一个 api 发生变化,需要修改用例文件

  • 可读性差:无法从代码中看出来明确的业务逻辑

  • 断言能力差:响应内容只能一层一层提取

2、 架构优化设计



goods.py 文件

import requestsfrom intermediate.apis.base_api import BaseApi

# 优化1:接口里面直接使用了 requests# 解决方案:在base_api 中添加公共的 send 方法,通过继承来调用# 优化2:大量重复的base_url# 在构造函数,实例化base_url, 并且在send方法, 就直接做拼接,就无需重复编写self.base_url# 优化3:token的获取也放在 BaseApi 的构造函数中class Goods(BaseApi):
def create(self, goods_data): create_url = "admin/goods/create" # r = requests.post(create_url, json=goods_data, headers={"X-Litemall-Admin-Token": self.token}) r = self.send("post", create_url, json=goods_data) return r
def list(self, goods_name, order="desc", sort="add_time"): goods_list_url = "admin/goods/list" goods_list_data = { "name": goods_name, "order": order, "sort": sort } # r = requests.get(goods_list_url, params=goods_list_data, headers={"X-Litemall-Admin-Token": self.token}) r = self.send("get", goods_list_url, params=goods_list_data) return r
def detail(self, goods_id): goods_detail_url = "admin/goods/detail" # r = requests.get(goods_detail_url, params={"id": goods_id}, headers={"X-Litemall-Admin-Token": self.token}) r = self.send("get", goods_detail_url, params={"id": goods_id}) return r
def detele(self, goods_id): delete_url = "admin/goods/delete" data = {"id": goods_id} r = self.send("post", delete_url, json=data) return r
复制代码

cart.py 文件

from intermediate.apis.base_api import BaseApi

class Cart(BaseApi): def add(self, goods_id, product_id): cart_url = "wx/cart/add" cart_data = {"goodsId": goods_id, "number": 1, "productId": product_id} r = self.send("post", cart_url, json=cart_data) return r
复制代码

base_api.py 文件

import jsonimport requestsfrom intermediate.utils.log_utils import logger

class BaseApi: # 如果有多角色,就做判断,如果没有就不做,即默认传None def __init__(self, base_url, role=None): self.base_url = base_url # 获取对应的角色信息 if role: self.role = role
# # ======管理端token获取 # login_b_url = "admin/auth/login" # login_b_data = {"username": "hogwarts", "password": "test12345", "code": ""} # r = self.send("post", login_b_url, json=login_b_data) # # 获取B端 token,定义为实例变量 # # self.token 的声明要在用例之前完成,使用 setup_class 提前完成变量的声明 # self.token_b = r["data"]["token"] # # # ======用户端token获取 # login_c_url = "wx/auth/login" # login_c_data = {"username": "user123", "password": "user123"} # login_c_r = self.send("post", login_c_url, json=login_c_data) # # 获取C端 token,定义为实例变量 # self.token_c = login_c_r["data"]["token"]
def __set_token(self, request_infos): """ 优化点:大部分接口都需要设置token 解决方案: 1、在发起接口请求之前,就获取token信息 2、获取token信息之后,塞入到请求信息之中 除了 method 和 url 之外,所有到其他信息,包括 header、params 等其他的参数,都会塞入至 kwargs 不定长参数中 cart_api(发起请求) —> 调用send方法 —> (send方法控制kwargs)获取 kwargs —> 在 kwargs 中塞入 header(鉴权)信息
优化点:有两个token如何解决 不同的角色有不同的token信息 :return: """ # ======管理端token获取 admin_url = "admin/auth/login" admin_data = {"username": "hogwarts", "password": "test12345", "code": ""} admin_r = requests.request("post", self.base_url + admin_url, json=admin_data) # 获取B端 token,定义为实例变量 # B端和C端token的key值不一样,可以将 self.token_b 变为键值对,加入相应的key self.token = {"X-Litemall-Admin-Token": admin_r.json()["data"]["token"]}
# ======用户端token获取 client_url = "wx/auth/login" client_data = {"username": "user123", "password": "user123"} client_r = requests.request("post", self.base_url + client_url, json=client_data) # 获取C端 token,定义为实例变量 self.client_token = {"X-Litemall-Admin-Token": client_r.json()["data"]["token"]}
# 如果是 admin,那么就塞入 admin 的 token,如果是其他,那么就塞入其他的 token # 在 test_cart 测试用例文件中,实例化时,需要传一个角色的值,确认是否是admin角色还是其他角色 if self.role == "admin": self.final_token = self.token else: self.final_token = self.client_token # 获取headers,如果请求本身有头信息,那么就把token信息更新(添加)进去 # if request_infos.get("headers"): # request_infos["headers"]["X-Litemall-Admin-Token"] = self.token_b # else: # request_infos["headers"] = {"X-Litemall-Admin-Token": self.token_b} if request_infos.get("headers"): request_infos["headers"].update(self.final_token) else: request_infos["headers"] = self.final_token
return request_infos
def send(self, method, url, **kwargs): self.__set_token(kwargs) r = requests.request(method, self.base_url + url, **kwargs) logger.debug(f"{url}接口的响应值为:{json.dumps(r.json(), indent=2, ensure_ascii=False)}") return r.json()
复制代码

test_cart.py 文件

import pytestfrom intermediate.apis.admin.goods import Goodsfrom intermediate.apis.wx.cart import Cart

@pytest.mark.parametrize("goods_name", ["0529goods01", "0529goods02"])class TestCart: def setup_class(self): self.goods = Goods("https://litemall.hogwarts.ceshiren.com/", "admin") self.cart = Cart("https://litemall.hogwarts.ceshiren.com/", "client")
def test_add_cart(self, goods_name): """ 添加购物车的步骤: 1、上架商品 2、获取商品列表 3、获取商品详情 4、添加购物车 """ goods_data = {"goods": {"picUrl": "", "gallery": [], "isHot": False, "isNew": True, "isOnSale": True, "goodsSn": "23052901", "name": goods_name}, "specifications": [{"specification": "规格", "value": "标准", "picUrl": ""}], "products": [{"id": 0, "specifications": ["标准"], "price": "59", "number": "59", "url": ""}], "attributes": []}
self.goods.create(goods_data) goods_list_r = self.goods.list(goods_name) self.goods_id = goods_list_r["data"]["list"][0]["id"] goods_detail_r = self.goods.detail(self.goods_id) product_id = goods_detail_r["data"]["products"][0]["id"] res = self.cart.add(self.goods_id, product_id) # 将delete放在用例中执行:1、调用方便;2、测试类中的每个方法并非都会添加goods数据; # 如果部分用例不添加,则相当于这个步骤,不能在每个测试用例执行完成之后执行,只能在单个用例执行完之后执行 self.goods.detele(self.goods_id) # 断言 assert res["errmsg"] == "成功"
复制代码

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 logging# 创建logger实例logger = logging.getLogger('simple_example')# 设置日志级别logger.setLevel(logging.DEBUG)# 流处理器ch = logging.StreamHandler()ch.setLevel(logging.DEBUG)# 日志打印格式formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')# 添加格式配置ch.setFormatter(formatter)# 添加日志配置logger.addHandler(ch)
复制代码

3、 添加领域模型


新增 domain 文件夹

新建 goods_domain.py 文件

class GoodsDomain:    """    抽象的概念,不做具体实现    代表是某一种业务模型    """    def detele_by_name(self, name):        # 如果子类 Goods 没有重写父类方法,是调不通的        # 调用子类方法        goods_list_r = self.list(name)        goods_id = goods_list_r["data"]["list"][0]["id"]        self.detele(goods_id)
复制代码

goods.py 文件变更


test_cart.py 文件变更

from intermediate.apis.wx.cart import Cart

# 不使用 self.goods.detele(self.goods_id)方法# 使用 self.goods.detele_by_name(goods_name) 方法调用delete接口@pytest.mark.parametrize("goods_name", ["0529goods01", "0529goods02"])class TestCart: def setup_class(self): self.goods = Goods("https://litemall.hogwarts.ceshiren.com/", "admin") self.cart = Cart("https://litemall.hogwarts.ceshiren.com/", "client")
def test_add_cart(self, goods_name): """ 添加购物车的步骤: 1、上架商品 2、获取商品列表 3、获取商品详情 4、添加购物车 """ goods_data = {"goods": {"picUrl": "", "gallery": [], "isHot": False, "isNew": True, "isOnSale": True, "goodsSn": "23052901", "name": goods_name}, "specifications": [{"specification": "规格", "value": "标准", "picUrl": ""}], "products": [{"id": 0, "specifications": ["标准"], "price": "59", "number": "59", "url": ""}], "attributes": []}
self.goods.create(goods_data) goods_list_r = self.goods.list(goods_name) self.goods_id = goods_list_r["data"]["list"][0]["id"] goods_detail_r = self.goods.detail(self.goods_id) product_id = goods_detail_r["data"]["products"][0]["id"] res = self.cart.add(self.goods_id, product_id) # 将delete放在用例中执行:1、调用方便;2、测试类中的每个方法并非都会添加goods数据; # 如果部分用例不添加,则相当于这个步骤,不能在每个测试用例执行完成之后执行,只能在单个用例执行完之后执行 # self.goods.detele(self.goods_id) self.goods.detele_by_name(goods_name) # 断言 assert res["errmsg"] == "成功"
复制代码


原文链接:https://ceshiren.com/t/topic/25559

发布于: 2023-06-01阅读数: 2
用户头像

测试人

关注

专注于软件测试开发 2022-08-29 加入

霍格沃兹测试开发学社,测试人社区:https://ceshiren.com/t/topic/22284

评论

发布
暂无评论
软件测试/测试开发丨接口测试实战学习笔记_程序员_测试人_InfoQ写作社区