总体思路
-
确定目标:1)作为冒烟case执行;2)代替部分手工回归的工作;3)线上主流程功能巡检。
-
确定接口自动化颗粒度:根据这个目标,我认为接口自动化需要做的比较精细。精细化是指:
- 全面的用例设计;
- 详细的断言,包括响应报文断言、落库后所有重要【表、字段】的校验;
- 核心场景case
-
设计流程:pytest执行一个接口脚本的流程:
- 脚本运行开始,先通过setup,对这个接口所有测试用例依赖的前置数据做初始化(新增到数据库等等);
- 每一条测试数据组装成1个http请求,向服务端发起请求;
- 每个请求结束之后,都会做断言;
- 这个接口的所有用例执行完成之后,做数据清理,保证脚本可重复执行。
-
设计框架
- 一个接口自动化项目包含所有微服务的接口
- 测试数据(testdata):按照服务名建立二级文件夹,存放以下测试数据
- 前置数据(excel)
- 测试用例HTTP请求内容(excel)
- 期望结果数据(查询接口的期望结果放在yaml文件,增删改接口的期望结果放在excel)
- 测试脚本(testcase):按照服务名建立二级文件夹,包含以下内容:
- 测试脚本。一个.py脚本只放一个接口
- conftest文件: 包含适用于这个服务下所有接口的前置数据初始化和后置数据清理,fixture级别设为module
- 公共方法(common):所有服务都共用的
- http请求方法封装(http_util.py)
- 各种数据库连接方法封装(database)
- excel文件处理方法封装
- yaml文件处理方法
- 断言方法封装
- 环境切换方法封装
- 配置文件(config)
- 测试、开发、生产环境的测试账号、服务ip、数据库连接等等
- 当前环境配置关键字
- 数据库和表结构的映射关系文件
- 全局conftest文件:包含以下内容
- 普通方法1:解决测试用例中文名乱码问题
- 普通方法2:接收命令行传入的参数,包括:环境参数、测试用例所属业务线、测试用例所属服务
- fixture方法1:获取普通方法2中接收到的环境参数,再修改配置文件中的环境关键字。作用域session,自动执行该方法
- 其他fixture方法:用于收集接口自动化执行的数据并写入数据库
-
选型
- Metersphere接口自动化平台
- TestNG+Rest Assured+Allure
- Pytest+Request+Allure
-
提效指标
- 自动化缺陷率:Q3(3%)
- 接口覆盖率:Q3(50%)
- 代码覆盖率:
- 接口覆盖粒度:单接口关联的测试用例数
-
提效结果
- 自动化缺陷率:Q3(4%)
- 接口覆盖率:Q3(87%)
实现过程中遇到的困难和解决方案
- 断言时的落库校验:一个功能涉及多个库的多个表的变更,读取期望结果excel表格的时候,只能获取到表名,无法知道这个表是哪个数据库的,故无法连接指定数据库查询实际结果。
- 建立从:表到数据库的映射文件,读取到excel中的表名之后,再根据mapper文件获取到数据库名,再进行数据库的连接、查询。
- 断言时,json结构比较复杂、层次比较深的响应报文校验
- 使用DeepDiff库,可以直接拿响应的json报文和期望的直接对比。
- 环境切换
- 开发、测试、生产环境的数据库、服务ip配置都不一样,需要根据执行用例时候的传入环境参数动态切换到指定的环境执行用例。
重点模块流程图
环境切换
环境配置

断言模块
使用的一些库
allure
参考文章
yaml文件处理(yaml包)
safe_load方法
# 先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对象,
openpyxl.load_workbook(file_name)
load_workbook参数解析
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对象
#返回结果是个worksheet对象
sheet=workbook[sheetname]
workbook.sheetnames方法
#获取excel所有的sheet名称 , 返回结果是个list
sheetnames = workbook.sheetnames
sheet[1]
#获取表格中的列名,获取第一行的所有cell对象
cell_row_first = sheet[1]
获取Excel表的列名
field_names = [cell.value for cell in sheet[1]]
获取表格中元素值
# 返回的是一个generator
values_rows = sheet.iter_rows(min_row=2, values_only=True)
遍历每一行元素值,进行处理
for row_data in values_rows:
# row_data是tuple类型的
# 将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方法
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参数的区别
- data参数
-
header不设置content-type
- data传入str类型的参数,默认的type是?(看代码实际运行结果,无type)
#代码 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。
#代码 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传入json(使用json.dumps将dict类型转成json),默认type是application/json;(待定)
-
header指定了Content-Type
- 无论data传入任何类型的对象,则type都是header指定的type,如下
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方法实践
# 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方法
response = requests.get(url=url, params=json.loads(query_params),
headers=header)
通常有以下2种传参方式
- Rest(pathvariables路径参数)
- Query(问号后面的参数)
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)
参考文章
连接MongoDB(pymongo库)
初始化Mongo连接对象
# 初始化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库中的所有集合(表)
result = self.connect_client[self.mongodbinfo['database']]
.list_collection_names()
筛选满足条件的一条记录
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
调用上述方法的代码
collection_name = 'tcollector'
mongodb = ConnectMongo().connect_mongodb(collection_name)
filters = {"taskid": taskid.lower()}
db_records = mongodb.fetch_one(collection_name, filters)
删除单条or多条记录
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(pymysql)
初始化mysql连接对象
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并返回结果
def query(self, sql):
self.cursor.execute(sql)
data = self.cursor.fetchall()
return data
执行增删改语句
def exec(self, *args, **kwargs):
self.cursor.execute(*args, **kwargs)
调用上述方法
# 调用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格式)
参考文章
读取文件(open)
常用方法
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)
常用的一些数据结构
字典
定义字典
person = {'name': 'Bob', 'age': 26, 'gender': 'Male'}
# 定义一个空字典
dict_a = {}
字典的遍历
- 同时遍历字典的key和value
# expect_record是一个字典。items方法会返回一个包含键和值的元组
for key_expect, value_expect in expect_record.items():
- 遍历所有的key
# 创建一个字典
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
# 创建一个字典
my_dict = {'a': 1, 'b': 2, 'c': 3}
# 使用 values() 方法遍历值
for value in my_dict.values():
print(f"值: {value}")
根据key获取内容
# 有2种方式可以获取内容
person.get('age')
person['name']
更新指定key对应的值
person['age'] = 27
添加元素到字典中
# key为'city'如果不存在,则会新增一个元素
person['city'] = 'Beijing'
遍历字典并修改
# 创建一个字典
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)
合并两个字典
a = {'a':1,'b':2}
b = {'b':3,'d':4}
c = a.update(b)
print(c)
print(a)
#结果:
None
{'a': 1, 'b': 3, 'd': 4}
列表
创建列表
list1 = [1, 2, 3, 4, 5.5, 6]
list2 = ['python', 'Python自学网', '后端学习']
list3 = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
['python', 'java']
]
增加元素
- append方法
li = ['xzc',[1,2,3],'123']
li.append('abc')
li.append(1)
print(li)
#输出['xzc',[1,2,3],'123','abc',1]
- insert方法
li = ['xzc',[1,2,3],'123']
li1 = li.insert(2,'ooo')
#在索引为2的'123'之前插入'ooo'
- extend方法
以最小元素追加,可迭代对象:字符串类型、列表等,Int类型不能迭代添加
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]
修改元素
- 单个修改
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
- 切片修改
li = ['xzc',[1,2,3],'123']
li[0:2] = '你好啊'
print(li) #输出['你','好','啊','123']
删除元素
- pop()方法:按照索引删除,并返回删除的元素
li = ['xzc',[1,2,3],'123']
name = li.pop(1) #删除[1,2,3]
print(name,li)#输出[1,2,3] ['xzc','123']
- remove()方法:按照元素删除,无返回值
li = ['xzc',[1,2,3],'123']
li.remove('xzc')#删除xzc
- clear()方法:清空列表
li = ['xzc',[1,2,3],'123']
li.clear() #清空
print(li) #输出[]
- del:直接删除列表
li = ['xzc',[1,2,3],'123']
del li
print(li)#此时输出列表会报错,因为已经被删除,列表不存在
- 切片删除
li = ['xzc',[1,2,3],'123']
# 左闭右开
del li[0:2] #删除'xzc',[1,2,3]
其他常用方法
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 元组
列表和元组的区别
-
可变性
- 列表(List)是可变的(Mutable),这意味着创建列表之后,你可以修改列表的内容,比如添加、删除或更改元素。列表使用方括号
[]定义,例如:my_list = [1, 2, 3]。由于其可变性,列表适合用于存储可能会改变的数据集合,如在程序运行期间动态修改其元素的情况。 - 元组(Tuple)是不可变的(Immutable),这意味着一旦元组被创建,它的内容就不能被改变。元组使用圆括号
()定义,例如:my_tuple = (1, 2, 3)。元组的不可变性使其特别适合用于存储不应该改变的数据,例如作为字典的键或函数返回多个值。尽管元组被认为是不可变的数据类型,但如果元组中包含的元素是可变类型(如字典),那么这些元素的值是可以被更改的(其实是内存地址不变)。
- 列表(List)是可变的(Mutable),这意味着创建列表之后,你可以修改列表的内容,比如添加、删除或更改元素。列表使用方括号
-
在创建元组时需注意,只创建1个元素,需在后面加逗号
在 Python 中,当你尝试创建只有一个元素的元组时,如果仅仅写成
(1),Python解释器会误解其为整数而非元组。这是因为在 Python 的语法中,圆括号()不仅用于定义元组,还常用来表示运算时的优先级。因此,当圆括号内部只包含一个单独的数值或者其他元素时,没有其他上下文指明这应该是一个元组的情况下,解释器就会将其视为普通的数学表达式中的圆括号,所以(1)被理解为数值1,实际是整数类型,而不是元组。为了让 Python 解释器明白你的意图是创建一个只有一个元素的元组,需要在该元素后面加上逗号
,。例如,(1, )就正确地被 Python 识别为一个元组,而不是整数 1。这里的逗号是关键,它告诉 Python 解释器,你正在定义的是一个元组,即使它只有一个元素。
字符串-split方法
Python的装饰器(decorator)
参考:Python 函数装饰器
概述和实例讲解
使用Python装饰器,可以通过函数修改另一个函数的功能。装饰器主要用了Python函数的3个特性
- 将函数作为参数传给另一个函数
- 在函数中定义函数
- 从函数中返回函数
#这段代码,用了函数的三个功能
#1)将函数作为参数传给另一个函数;2)在函数中定义函数;3)从函数中返回函数
# a_new_decorator是装饰器函数;
# a_function_requiring_decoration是被装饰的函数(被修改功能的函数)
def a_new_decorator(a_func):
def wrapTheFunction():
print("I am doing some boring work before executing a_func()")
a_func()
print("I am doing some boring work after executing a_func()")
return wrapTheFunction
def a_function_requiring_decoration():
print("I am the function which needs some decoration to remove my foul smell")
a_function_requiring_decoration()
#outputs: "I am the function which needs some decoration to remove my foul smell"
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
#now a_function_requiring_decoration is wrapped by wrapTheFunction()
a_function_requiring_decoration()
#outputs:I am doing some boring work before executing a_func()
# I am the function which needs some decoration to remove my foul smell
# I am doing some boring work after executing a_func()
使用@符号进行装饰
@a_new_decorator
def a_function_requiring_decoration():
"""Hey you! Decorate me!"""
print("I am the function which needs some decoration to "
"remove my foul smell")
a_function_requiring_decoration()
#outputs: I am doing some boring work before executing a_func()
# I am the function which needs some decoration to remove my foul smell
# I am doing some boring work after executing a_func()
#the @a_new_decorator is just a short way of saying:
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
自动化实践中的应用
- @pytest.fixture
- @pytest.mark.parameterize
- @pytest.skip?
*args 与 **kwargs
cloud.tencent.com/developer/a…
pytest相关
pytest执行用例的方式
常用命令行参数
# 传入环境参数env
pytest -vs
--test_project=im
--service_name=im-chat-service
--env=dev
pytest装饰器
pytest.fixture()
fixture的执行顺序
【pytest官方文档】解读fixtures - 11. fixture的执行顺序,3要素详解(长文预警) - 把苹果咬哭的测试笔记 - 博客园
pytest_addoption
pytest_addoption 是 Pytest 测试框架提供的一个钩子函数。在编写测试用例时,可以使用该钩子函数来定义自定义的命令行选项。
通常情况下,Pytest 框架会自动解析 -h 或 --help 选项,并显示帮助信息。但是,如果我们需要添加一些额外的命令行选项来配置测试环境或传递参数给测试用例,就可以使用 pytest_addoption 函数。
通过在测试模块中定义 pytest_addoption 函数,我们可以通过调用 parser.addoption 方法来定义自定义选项。这些自定义选项将可用于在运行测试时从命令行传递参数。
以下是一个示例:
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 选项来指定测试的环境。 例如:
pytest --url=http://myapp.com --env=staging
然后,在测试代码中,可以通过解析命令行选项来获取这些参数:
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 函数。这样,在运行测试时就能够使用自定义的命令行选项。


常见面试题
根据目前遇到的面试来说,可以归为以下几大类
- 做框架的目的、思路
- 框架有哪些模块、功能
- 遇到的难点,如何解决的
- 做这个事情的过程中做的比较好的地方
- 接口自动化的稳定性问题
- 接口自动化的收益
- 一些实现的细节、技术问题
以下是具体的面试题:
- 做接口自动化框架主要是想解决什么问题/目的是什么
- 自动化测试框架有哪些功能
- 为什么选pytest框架,有没有和其他框架对比过
- 自动化这块你感觉你了解的多吗?还是说你现在可以独立的搭建一个自动化框架
- 接口自动化你是怎么做的
- 原回答:您是想了解这个框架里面主要包含哪些功能模块吗
- 面试官:用什么语言,用什么框架,以及一些基础的方法都是怎么用的
- 你的接口自动化是链路自动化集成的还是单个接口的测试
- pytest装饰器用了哪些
- pytest的fixture作用范围
- 如果有两个fixture,一个范围是class,一个是function,应该先执行哪个
- allure怎么显示入参和出参
- allure用了哪些功能
- allure怎么显示测试步骤
- request用了哪些请求
- post传body,有哪些格式
- 通过data参数传进去,通过json参数传进去
- post要在header增加token,怎么传
- 通过headers参数传进去
- 自动化的尝试,考虑哪些实现点,要解决什么问题
- 这个框架是怎么设计的以及前期是怎么实现的,包括框架选型之类的
- 这个框架里已经涵盖了大部分的51闪聘的接口吗
- 登录模块是怎么封装业务接口的(登录模板作为前置操作)
- 测试环境的测试账号和密码是怎么配置的
- 怎么设置断言的
- 能简单举例说下某个业务接口返回哪些参数格式吗
- 接口返回的数据,做断言校验的时候是怎么解析的
- 目前的自动化是怎么做的,你们项目里面不涉及吗
- 你在做自动化框架,学习的过程中碰到的问题以及你觉得你做的比较好的地方
- python自动化框架实现的思路
- 工作中有没有写过什么工具,框架或者产品
- 介绍一下你做的这个框架
- 框架的难点在什么地方
- 你觉得数据库校验的难点在哪里
- 你的自动化框架是怎么写的
- get和post的区别
- http请求头里有哪些内容
- 介绍一下接口自动化框架
- 数据驱动怎么做的,yaml文件里的测试数据怎么写的
- 如果一个接口依赖于其他接口,这种是怎么做数据驱动的
- http协议有了解吗,
- pytest有哪些特性/可以使用pytest的哪些功能去做测试
- pytest的fixture的作用是什么
- 数据驱动的好处和作用
- 有哪些功能手工做不了,但是可以通过数据驱动来完成
- request库,用了哪些:发送get和post请求
- post请求的请求体有哪些数据格式/协议格式?
- 自动化的时候测试用例是怎么组织的
- 接口自动化框架的设计思路
- 环境切换怎么做的
- python的list和tuple的区别
- python有个代码静态检查的工具,你知道吗
- python你用过其他的代码框架吗
- python有哪些内置的数据类型
- 你了解python的decorator吗
- 介绍一下接口自动化框架
- 框架里的数据怎么存储的
- 接口名称也是在excel文件里吗
- 前置数据会做什么处理
- 接口的逻辑里面需要关联多个表的时候,前置数据怎么插入
- 你这里写的环境切换的功能是什么
- 你们公司会有一个整体的框架吗,还是你们各个小组自己整自己的
- 接口用例跑的成功率怎么样
- 面试官补充:一个接口没过,是接口设计的问题,还是代码就有bug。比如你写了100个case,是跑通了100个,还是有失败的
- 这个调整是你来调整的吗
- 除了功能测试和自动化测试,还会有一些其他的测试经验吗
- 做接口自动化的思路是什么
- 接口自动化里面读取excel文件用的哪个库
- 接口自动化测试的稳定性怎么解决
- 接口自动化代码结构
- 接口自动化怎么做的
- 自动化框架分哪几个大模块,你负责哪几个模块
- 接口自动化实现过程中遇到的问题
- 测试过程中怎么构造测试数据
- 接口自动化写了多少用例,运行的稳定性、覆盖率怎么样,会去做一些类和方法的排除吗
- 整体case的稳定性怎么样,成功率有多少,有问题的case大概是什么原因
- 用例是定时跑的吗,这个过程会有失败吗
- 自动化本身运行会有问题吗
- 做自动化平时投入的维护成本和做回归测试,这两个收益对比来看,哪个更大
- case挂了,谁负责排查