Pytest接口自动化实战总结

重点模块流程图

环境切换

环境配置

断言模块

使用的一些库

yaml文件处理(yaml包)

safe_load方法

python 复制代码
# 先open方法,打开文件,再将文件加载为python对象,
# 如果原yaml文件里是字典模式,那返回结果就是字典;如果原来是列表模式,那就返回列表
def read_yaml(self, file):
    with open(file, 'r', encoding='utf-8') as file:
        data = yaml.safe_load(file)
    return data

excel文件处理(openpyxl

使用的地方:读取excel文件中的测试数据,并以字典格式返回

openpyxl库中常用的类

Workbook:表示一个Excel工作簿,是操作Excel文件的基础类。通过它,可以创建新的工作簿,打开已有的工作簿,以及保存工作簿等。 Worksheet:表示一个Excel工作表,是对单个表格的操作类。通过它,可以读取和修改单元格的值,设置单元格的格式等。 Cell:表示一个Excel单元格,是对单个单元格的操作类。通过它,可以读取和修改单元格的值,获取单元格的行和列等信息。 Chart:表示一个Excel图表,是对图表的操作类。通过它,可以创建图表,设置图表的类型、数据源、位置等。 Style:表示一种单元格格式,是对单元格格式的操作类。通过它,可以设置单元格的字体、颜色、对齐方式等格式。

load_workbook方法

读取excel表格,并返回一个workbook对象,

python 复制代码
openpyxl.load_workbook(file_name)

load_workbook参数解析

python 复制代码
openpyxl.load_workbook
(filename, read_only=False, keep_vba=False, data_only=False, keep_links=True, **kwargs)
  • filename: 必需。要加载的Excel文件的路径或文件名。可以是相对路径或绝对路径。

  • read_only: 可选。布尔值,默认为 False。如果设置为 True,则以只读模式打开文件,不允许进行修改。如果设置为 False,则以读写模式打开文件,可以读取和修改数据。

  • keep_vba: 可选。布尔值,默认为 False。如果设置为 True,则在加载文件时保留 VBA 代码。如果设置为 False,则在加载文件时删除 VBA 代码。

  • data_only: 可选。布尔值,默认为 False。如果设置为 True,则只加载数据,不加载样式、公式等信息。如果设置为 False,则加载所有数据和样式。

  • keep_links: 可选。布尔值,默认为 True。如果设置为 True,则在加载文件时保留链接。如果设置为 False,则在加载文件时删除链接。

  • **kwargs: 可选。用于传递其他额外的参数给 openpyxl.load_workbook() 方法。这些参数的具体用途和用法取决于 openpyxl 库的版本和功能。

workbook[sheetname]方法

根据传入的sheet页名称,返回sheet对象

python 复制代码
#返回结果是个worksheet对象
sheet=workbook[sheetname]

workbook.sheetnames方法

python 复制代码
#获取excel所有的sheet名称 , 返回结果是个list
sheetnames = workbook.sheetnames

sheet[1]

python 复制代码
#获取表格中的列名,获取第一行的所有cell对象
cell_row_first = sheet[1]

获取Excel表的列名

python 复制代码
field_names = [cell.value for cell in sheet[1]]

获取表格中元素值

python 复制代码
values_rows = sheet.iter_rows(min_row=2, values_only=True)

遍历每一行元素值,进行处理

python 复制代码
for row_data in values_rows:
    # 将excel中的空白值(读出来之后是None类型)替换为空字符串
    row = tuple('' if value is None else value for value in row_data)

HTTP请求处理

请求头header

请求头中的contentType有3种常见的类型,对应于post请求的3种body设置

  • application/x-www-form-urlencoded
  • application/json
  • multipart/form-data

POST方法

python 复制代码
def post(url, data=None, json=None, **kwargs):
    r"""Sends a POST request.

  :param url: URL for the new :class:`Request` object.
  :param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
  :param json: (optional) json data to send in the body of the :class:`Request`.
  :param **kwargs: Optional arguments that ``request`` takes.
  :return : :class:`Response <Response>` object
  :rtype : requests.Response
"""

return request("post", url, data=data, json=json, **kwargs)

通常有以下3种传参

  • Query(问号后面的参数)
  • body
    • application/x-www-form-urlencoded
    • application/json
    • multipart/form-data(可传文件)

data和json参数的区别

若不设置默认的Content-Type,则

  • data传入参数是str,默认的type是?(看代码实际运行结果,无type)
python 复制代码
#代码
url = 'http://httpbin.org/post'
# data = {'a_test': 112233, 'b_test': 223344}
data = '123'
print(type(data))

r = requests.post(url=url, data=data).json()
pprint(r)

#响应结果
<class 'str'>
{'args': {},
 'data': '123',
 'files': {},
 'form': {},
 'headers': {'Accept': '*/*',
             'Accept-Encoding': 'gzip, deflate',
             'Content-Length': '3',
             'Host': 'httpbin.org',
             'User-Agent': 'python-requests/2.28.2',
             'X-Amzn-Trace-Id': 'Root=1-6661be67-57fe55d32da413b669ddfda3'},
 'json': 123,
 'origin': '101.80.29.22',
 'url': 'http://httpbin.org/post'}
  • data传入dict,默认的type是application/x-www-form-urlencoded,而且参数进入了form。
python 复制代码
#代码
url = 'http://httpbin.org/post'
data = {'a_test': 112233, 'b_test': 223344}
# data = '123'
print(type(data))

r = requests.post(url=url, data=data).json()
pprint(r)

# 响应结果
{'args': {},
 'data': '',
 'files': {},
 'form': {'a_test': '112233', 'b_test': '223344'},
 'headers': {'Accept': '*/*',
             'Accept-Encoding': 'gzip, deflate',
             'Content-Length': '27',
             'Content-Type': 'application/x-www-form-urlencoded',
             'Host': 'httpbin.org',
             'User-Agent': 'python-requests/2.28.2',
             'X-Amzn-Trace-Id': 'Root=1-6661bdf6-38fe95c02238d1412414d5e8'},
 'json': None,
 'origin': '101.80.29.22',
 'url': 'http://httpbin.org/post'}
  • data传入任何类型的对象,只要header指定了Content-Type,则type是指定的type,如下
python 复制代码
from pprint import pprint
import requests

url = 'http://httpbin.org/post'
# data = {'a_test': 112233, 'b_test': 223344}
header = {}
header['Content-Type'] = 'applicaltion/json'
data = '123'
print(type(data))

r = requests.post(url=url, data=data, headers=header).json()
pprint(r)

# 打印的响应结果
{'args': {},
 'data': '123',
 'files': {},
 'form': {},
 'headers': {'Accept': '*/*',
             'Accept-Encoding': 'gzip, deflate',
             'Content-Length': '3',
             'Content-Type': 'applicaltion/json',
             'Host': 'httpbin.org',
             'User-Agent': 'python-requests/2.28.2',
             'X-Amzn-Trace-Id': 'Root=1-66669dbb-450def7e76597530703d0a65'},
 'json': 123,
 'origin': '101.80.29.21',
 'url': 'http://httpbin.org/post'}
  • json传入任何类型的对象,或者即使header传入了指定的content-Type,实际传输的也是application/json

POST方法实践

python 复制代码
# post请求仅传query参数的时候:
if query_params != "{}":
    query_dict = json.loads(query_params)
    query_string = urlencode(query_dict)
    url = url + "?" + query_string
    response = requests.post(url, headers=header)
else: # post请求传body参数,要转为json格式,
    response = requests.post(url, data=json.dumps(process_data), headers=header)

GET方法

python 复制代码
response = requests.get(url=url, params=json.loads(query_params), 
headers=header)

通常有以下2种传参方式

  • Rest(pathvariables路径参数)
  • Query(问号后面的参数)
python 复制代码
if method.lower() == 'get':
    response = requests.get(url=url, params=json.loads(query_params), headers=header)
elif method.lower() == 'post':
    if query_params != "{}":
        query_dict = json.loads(query_params)
        query_string = urlencode(query_dict)
        url = url + "?" + query_string
        response = requests.post(url, headers=header)
    else:
        response = requests.post(url, data=json.dumps(process_data), headers=header)
elif method.lower() == 'put':
    response = requests.put(url, data=json.dumps(process_data), headers=header)

参考文章

  1. requet.post请求中的data和json参数的区别

连接MongoDB(pymongo库)

初始化Mongo连接对象

python 复制代码
# 初始化Mongo连接对象
if self.mongodbinfo.get("password") is None:
    url = "mongodb://%(host)s:%(port)s" % {
        "host": self.mongodbinfo['host'],
        "port": self.mongodbinfo['port'],
    }
else:
    # 扩展有密码时连接的配置信息;
    url = "mongodb://%(user)s:%(password)s@%(host)s:%(port)s/?authSource=%(database)s" % {
        "user": self.mongodbinfo["user"],
        "password": self.mongodbinfo["password"],
        "host": self.mongodbinfo["host"],
        "port": self.mongodbinfo["port"],
        "database": self.mongodbinfo["database"]
    }
# print(url)
# 将线程安全的连接池封装到对象中;
self.connect_client = pymongo.MongoClient(url)

获取指定Mongo库中的所有集合(表)

python 复制代码
result = self.connect_client[self.mongodbinfo['database']]
            .list_collection_names()

筛选满足条件的一条记录

python 复制代码
def fetch_one(self, collection_name: str, filters: dict = None) -> dict:
    """
    查询一条符合条件的数据信息
    :param collection_name: 集合的名称;
    :param filters: dict; 过滤条件;
    :return: dict; 筛选结果,字典信息;

    example:
        filters = {"name": "python入门"}
        v = mongo_helper.fetch_one("test", filters)
        print(v)

    """
    conn = self.connect_client[self.mongodbinfo['database']][collection_name]
    result = conn.find_one(filters)
    # self.close_connect()
    return result

调用上述方法的代码

python 复制代码
collection_name = 'tcollector'
mongodb = ConnectMongo().connect_mongodb(collection_name)
filters = {"taskid": taskid.lower()}
db_records = mongodb.fetch_one(collection_name, filters)

删除单条or多条记录

python 复制代码
def delete_one(self, collection_name: str, filters: dict) -> int:
    """
    删除单条的数据信息;
    :param collection_name:
    :param filters:
    :return: int; 删除数据的条数;

    example:
        filters = {"name": "批量修改回来"}
        v = mongo_helper.delete_one("test", filters)
        print(v, type(v))
    """
    conn = self.connect_client[self.mongodbinfo['database']][collection_name]
    result = conn.delete_one(filter=filters)
    # self.close_connect()
    return result.deleted_count

def delete_many(self, collection_name: str, filters: dict) -> int:
    """
    删除多条的数据信息;
    :param collection_name: 集合的名称;
    :param filters: dict; 过滤条件;
    :return: int; 返回删除的条数;

    example:
        filters = {"name": "批量修改回来"}
        v = mongo_helper.delete_many("test", filters)
        print(v, type(v))

    """
    conn = self.connect_client[self.mongodbinfo['database']][collection_name]
    result = conn.delete_many(filter=filters)
    # self.close_connect()
    return result.deleted_count

连接MySql

初始化mysql连接对象

python 复制代码
def __init__(self, dbInfo):
    self.host = dbInfo['host']
    self.port = dbInfo['port']
    self.db = dbInfo['db']
    self.user = dbInfo['user']
    self.password = dbInfo['password']
    self.charset = 'utf8'
  
def connect(self, restype='dict'):
    self.close()
    self.conn = pymysql.connect(host=self.host,
                                port=self.port,
                                database=self.db,
                                user=self.user,
                                password=self.password,
                                charset=self.charset,
                                autocommit=True)
    if restype == 'dict':
        self.cursor = self.conn.cursor(cursor=pymysql.cursors.DictCursor)
    else:
        self.cursor = self.conn.cursor()

查询指定sql并返回结果

python 复制代码
def query(self, sql):
    self.cursor.execute(sql)
    data = self.cursor.fetchall()
    return data

执行增删改语句

python 复制代码
def exec(self, *args, **kwargs):
    self.cursor.execute(*args, **kwargs)

调用上述方法

python 复制代码
# 调用query方法
sql = "SELECT top 3 * FROM Form"
db = ConnectDB().connect_db('Form')
res = db.query(sql)
print(res)

# 调用exec方法
for tablename in tablenames:
    dbs = ConnectDB().connect_db(tablename)
    # 构造数据库的删除语句
    if len(self.condition) != 0:
        query_sql = " Delete FROM " + tablename + self.condition

    if order_by is not None:
        query_sql += self.order_by

    # 兼容相同表名对应多个库的情况(有分库)
    if isinstance(dbs, list):
        for db in dbs:
            # 删除用exec
            db.exec(query_sql)
    else:
        dbs.exec(query_sql)

Json库

json.loads()方法

将字符串转成Python对象(目前接口自动化项目中常用的是将json格式的字符串转为dict)

json.dumps()方法

将传入的Python对象转成字符串(项目中常用的是将dict对象转为json格式)

参考文章

  1. Python中json模块的load/loads方法实战及参数详解

读取文件(open)

常用方法

python 复制代码
def modify_evn_conf(self, file_path, env):
    with open(file_path, 'r', encoding='utf-8') as f:
        data = yaml.safe_load(f)

    data['env'] = env
    with open(file_path, 'w') as f:
        yaml_str = yaml.dump(data)
        yaml_str = yaml_str.rstrip('\n')
        # 修改文件内容
        f.write(yaml_str)

常用的一些数据结构

字典

定义字典

python 复制代码
person = {'name': 'Bob', 'age': 26, 'gender': 'Male'}
# 定义一个空字典
dict_a = {}

字典的遍历

添加元素到字典中 person['city'] = 'Beijing' print(person) 输出 {'name': 'Bob', 'age': 27, 'gender': 'Male', 'city': 'Beijing'}

删除字典中的元素 del person['gender'] print(person) 输出 {'name': 'Bob', 'age': 27, 'city': 'Beijing'}

字典的遍历

  • 同时遍历字典的key和value
python 复制代码
# expect_record是一个字典。items方法会返回一个包含键和值的元组
for key_expect, value_expect in expect_record.items():
  • 遍历所有的key
python 复制代码
# 创建一个字典
my_dict = {'a': 1, 'b': 2, 'c': 3}

# 遍历字典的键
for key in my_dict:
    print(f"键: {key}, 值: {my_dict[key]}")


# 使用 keys() 方法遍历键
for key in my_dict.keys():
    print(f"键: {key}")
  • 遍历所有的value
python 复制代码
# 创建一个字典
my_dict = {'a': 1, 'b': 2, 'c': 3}

# 使用 values() 方法遍历值
for value in my_dict.values():
    print(f"值: {value}")

根据key获取内容

python 复制代码
# 有2种方式可以获取内容
person.get('age')
person['name']

更新指定key对应的值

python 复制代码
person['age'] = 27

添加元素到字典中

python 复制代码
# key为'city'如果不存在,则会新增一个元素
person['city'] = 'Beijing'

遍历字典并修改

ini 复制代码
# 创建一个字典
my_dict = {'a': 1, 'b': 2, 'c': 3}

# 遍历字典并修改
for key in list(my_dict.keys()):  # 使用 list 来避免在遍历中修改字典
    if my_dict[key] < 2:
        del my_dict[key]

print(my_dict)

合并两个字典

update方法

列表

创建列表

python 复制代码
list1 = [1, 2, 3, 4, 5.5, 6]
list2 = ['python', 'Python自学网', '后端学习']
list3 = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
    ['python', 'java']
]

增加元素

  • append方法
python 复制代码
li = ['xzc',[1,2,3],'123']
li.append('abc')
li.append(1)
print(li)
#输出['xzc',[1,2,3],'123','abc',1]
  • insert方法
python 复制代码
li = ['xzc',[1,2,3],'123']
li1 = li.insert(2,'ooo')
#在索引为2的'123'之前插入'ooo'
  • extend方法

以最小元素追加,可迭代对象:字符串类型、列表等,Int类型不能迭代添加

python 复制代码
li = ['xzc',[1,2,3],'123']
li2 = li.extend('哈喽')
print(li2)
#结果['xzc',[1,2,3],'123','哈','喽']

li3 = li.extend([1,2,3])
print(li3)
#结果['xzc',[1,2,3],'123',1,2,3]

修改元素

  • 单个修改
python 复制代码
li = ['xzc',[1,2,3],'123']
li[0] = 'sun' #把xzc改成sun

#利用replace()方法
li[0] = li[0].capitalize()  #sun的首字母大写,再放回原处
li[0] = li[0].replace('x','a')  #把'xzc'找出来,然后把x换成a
  • 切片修改
python 复制代码
li = ['xzc',[1,2,3],'123']
li[0:2] = '你好啊'
print(li) #输出['你','好','啊','123']

删除元素

  • pop()方法:按照索引删除,并返回删除的元素
python 复制代码
li = ['xzc',[1,2,3],'123']
name = li.pop(1) #删除[1,2,3]
print(name,li)#输出[1,2,3] ['xzc','123']
  • remove()方法:按照元素删除,无返回值
python 复制代码
li = ['xzc',[1,2,3],'123']
li.remove('xzc')#删除xzc
  • clear()方法:清空列表
python 复制代码
li = ['xzc',[1,2,3],'123']
li.clear() #清空
print(li) #输出[]
  • del:直接删除列表
python 复制代码
li = ['xzc',[1,2,3],'123'] 
del li
print(li)#此时输出列表会报错,因为已经被删除,列表不存在
  • 切片删除
python 复制代码
li = ['xzc',[1,2,3],'123']
# 左闭右开
del li[0:2] #删除'xzc',[1,2,3]

其他常用方法

python 复制代码
li = ['xzc',[1,2,3],'123]
# 输出列表的长度
print(len(i))
# 指定元素出现的次数
li.count('xzc') 
# 寻找指定元素的索引
li.index('xzc')
# 排序(默认从小到大)
li = [1,5,6,9,8,7]
li.sort()
# 逆向排序(从大到小)
li.sort(reverse=True)
# 列表反转
li.reverse()

元组

参考:Python 元组

字符串-split方法

参考:Python split()方法

pytest相关

fixture的执行顺序

【pytest官方文档】解读fixtures - 11. fixture的执行顺序,3要素详解(长文预警) - 把苹果咬哭的测试笔记 - 博客园

pytest_addoption

pytest_addoption 是 Pytest 测试框架提供的一个钩子函数。在编写测试用例时,可以使用该钩子函数来定义自定义的命令行选项。

通常情况下,Pytest 框架会自动解析 -h 或 --help 选项,并显示帮助信息。但是,如果我们需要添加一些额外的命令行选项来配置测试环境或传递参数给测试用例,就可以使用 pytest_addoption 函数。

通过在测试模块中定义 pytest_addoption 函数,我们可以通过调用 parser.addoption 方法来定义自定义选项。这些自定义选项将可用于在运行测试时从命令行传递参数。

以下是一个示例:

python 复制代码
content of conftest.py

def pytest_addoption(parser):
    parser.addoption("--url", action="store", default="http://example.com",
                     help="Specify the URL for the tests")
    parser.addoption("--env", action="store", default="qa",
                     choices=["qa", "staging", "prod"],
                     help="Specify the environment for the tests")

在这个例子中,我们定义了两个自定义选项 --url 和 --env。在运行测试时,可以使用 --url 选项来指定测试的 URL,使用 --env 选项来指定测试的环境。 例如:

bash 复制代码
pytest --url=http://myapp.com --env=staging

然后,在测试代码中,可以通过解析命令行选项来获取这些参数:

ini 复制代码
content of test_example.py

def test_something(request):
    url = request.config.getoption("--url")
    env = request.config.getoption("--env")
    # 使用 url 和 env 进行测试

通过解析 request 对象的 config 属性,我们可以使用 getoption 方法获取命令行选项的值,并将其用于测试逻辑中。

总结来说,pytest_addoption 函数允许我们在 Pytest 测试框架中定义自定义的命令行选项,以方便地配置和定制测试环境。

Pytest 框架会自动识别和执行 conftest.py 文件中的钩子函数,包括 pytest_addoption 函数。

当 Pytest 执行测试时,会按照一定的顺序搜索当前目录及其父级目录下的所有 conftest.py 文件。一旦找到了一个 conftest.py 文件,它就会加载其中的钩子函数和其他配置。

在加载 conftest.py 文件时,Pytest 会检查文件中是否存在定义了特定名称的钩子函数。如果发现了 pytest_addoption 函数定义,Pytest 就会调用该函数并将一个 parser 对象作为参数传递给它。

parser 对象是 Pytest 内部的一个配置解析器,它允许我们使用 addoption 方法来定义自己的命令行选项。

因此,只要将 pytest_addoption 函数定义在 conftest.py 文件中,并确保 conftest.py 文件与需要使用这些选项的测试模块在同一目录或其父级目录中,Pytest 就能够自动识别和执行 pytest_addoption 函数。这样,在运行测试时就能够使用自定义的命令行选项。

相关推荐
Null箘1 分钟前
从零创建一个 Django 项目
后端·python·django
秋意钟11 分钟前
Spring新版本
java·后端·spring
小蜗牛慢慢爬行31 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
A小白59081 小时前
Docker部署实践:构建可扩展的AI图像/视频分析平台 (脱敏版)
后端
goTsHgo1 小时前
在 Spring Boot 的 MVC 框架中 路径匹配的实现 详解
spring boot·后端·mvc
waicsdn_haha1 小时前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
Q_19284999061 小时前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
良许Linux1 小时前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
ThisIsClark2 小时前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
求知若饥2 小时前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs