『居善地』接口测试 — 7、接口自动化测试框架的设计与实现
(一)接口测试框架的思想
自动化测试框架不是一个模式,而是一种思想和方法的集合,通俗的讲就是一个架构。
为了更好的了解自动化测试框架,应该对以下几种自动化测试框架思想有一定的认知:
- 模块化思想
- 库思想
- 数据驱动思想
- 关键字驱动思想
以上仅仅是代表了一种自动化测试的思想,并不能定义为框架。
上面讲到框架=思想+方法,于是演化了以下五种框架:
1、模块化测试脚本框架
需要创建小而独立的可以描述的模块、片断以及待测应用程序的脚本。
这些树状结构的小脚本组合起来,就能组成能用于特定的测试用例的脚本。
2、测试库框架
与模块化测试脚本框架很类似,并且具有同样的优点。
不同的是测试库框架把待测应用程序分解为过程和函数而不是脚本。
框架需要创建描述模块、片断以及待测应用程序的功能库文件。
3、关键字驱动或表驱动的测试框架
框架需要开发数据表和关键字。
这些数据表和关键字独立于执行它们的测试自动化工具,并可以用来“驱动"待测应用程序和数据的测试脚本代码,关键宇驱动测试看上去与手工测试用例很类似。
在一个关键字驱动测试中,把待测应用程序的功能和每个测试的执行步骤一起写到一个表中。
测试框架可以通过很少的代码来产生大量的测试用例。
同样的代码在用数据表来产生各个测试用例的同时被复用。
4、数据驱动测试框架
在这里测试的输入和输出数据是从数据文件中读取(数据池,ODBC源,CSV文件,EXCEL文件,Json文件,Yaml文件,ADO对象等)并且通过捕获工具生成或者手工生成的代码脚本被载入到变量中。
在这个框架中,变量不仅被用来存放输入值还被用来存放输出的验证值。
整个程序中,测试脚本来读取数值文件,记载测试状态和信息。这类似于表驱动测试,在表驱动测试中,它的测试用例是包含在数据文件而不是在脚本中,对于数据而言,脚本仅仅是一个“驱动器”,或者是一个传送机构。
然而数据驱动测试不同于表驱动测试,尽管导航数据并不包含在表结构中。
在数据驱动测试中,数据文件中只包含测试数据。
5、混合测试自动化框架
最普遍的执行框架是上面介绍的所有技术的一个结合,取其长处,弥补其不足。
混合测试框架是由大部分框架随着时间并经过若干项目演化而来的。
(二)接口测试框架结构解析
common目录:
一些公共方法存放目录。- 封装请求
send_method.py
:封装接口请求方法。 - 封装获取返回值
getKeyword_forResult.py
:通过关键字获取接口返回值。 - 读取数据方法
- 封装请求
interface目录:
存放接口的目录。
每一个接口或者一类接口来写一个interface(也就是一个接口对应一个.py文件)- 对该接口的请求:用于单接口测试
- 根据业务获取接口返回值:用于关联接口测试
script目录:
存放测试用例的目录。
也可以命令为testCase目录。
接口测试用例包括:- 单接口测试用例
- 关联接口测试用例
Config目录:
存放配置文件。配置一些常量,例如数据库的相关信息,接口的相关信息等。Data目录:
存放公共部分数据,比如测试数据,excel文件等等。Log目录:
存放logging日志信息。Reports目录:
存放接口测试报告目录。runMain.py文件:
主程序入文件口,用于执行case。
(三)接口自动化测试框架封装实现
之前分析完了接口测试框架的设计与架构,下面我们就来一步一步的完成接口自动化测试框架的实现。
1、创建测试框架项目
为Student Management System Interface testing framework
创建一个测试项目SMSITF
。
项目名上右键 —> New —> Python Package —> 创建common目录。
同理创建如下目录:
interface
目录:存放接口的目录。script
目录:存放测试用例的目录。Config
目录:存放配置文件。配置一些常量,例如数据库的相关信息,接口的相关信息等。Data
目录:存放公共部分数据,比如测试数据,excel文件等等。Log
目录:存放logging日志信息。Reports
目录:存放接口测试报告目录。
创建好后如下图:
接下来我们要一步一步实现这个框架里边的功能。
Dictionary
和Python Package
目录说明:
Dictionary
在Pycharm中就是一个文件夹,放置资源文件,该文件夹其中并不包含__init.py__
文件。
Python Package
文件夹会自动创建__init.py__
文件,换句话说Python Package
就是创建一个目录,其中包括一组模块和一个__init.py__
文件。
2、封装发送请求方法
一些公共的方法,要写在common
目录中,主要是封装使用Requests库发送请求的方法。
其他所有的公共的方法都可以封装在common
目录中。
"""
send_method.py 文件说明:
1,封装接口请求方式
根据项目接口文档提供的内容进行封装
不同的项目,sendmethod也不太一样,如请求体格式等。
2.封装思路-结合接口三要素
请求方式+请求地址
请求参数
返回值
3.以学生管理系统SMS为例:
结合学生管理系统项目的接口文档,封装SendMethod类
"""
# 导入所需模块
import requests
import json
# 封装请求模块
class SendMethod:
"""
结合学生管理系统SMS,请求方式包括如下:
get ---> parmas标准请求参数
post--->请求参数类型 json
put --->请求参数类型 json
delete ---> parmas标准请求参数
"""
# 定义该方法为静态方法
@staticmethod
def send_method(method, url, parmas=None, json=None):
"""
封装适用于学生管理系统项目的接口请求
:param method: 请求方式
:param url: 请求地址
:param parmas: get和delete请求参数
:param json: post和put请求参数
:param headers: 请求头
:return:
"""
# 定义发送请求的方法
if method == "get" or method == "delete":
response = requests.request(method=method, url=url, params=parmas)
elif method == "post" or method == "put":
response = requests.request(method=method, url=url, json=json)
# 如果有不同的请求头,还可以继续添加接收的参数
# response = requests.request(method=method, url=url, json=json, data=data, files=data)
else:
# 这里是简单处理,完成封装需要加上异常处理。
response = None
print("请求方式不正确")
# 如果请求方式是delete,只返回状态码
# 这是根据项目接口文档中delete方法的返回规则定的。
if method == "delete":
return response.status_code
else:
# 项目中接口的返回值是json格式的,就可以用json()进行格式化返回结果。
return response.json()
@staticmethod
def json_2_python(res):
"""
格式化返回数据
:param res:接口返回的数据
:return:
"""
return json.dumps(res, indent=2, ensure_ascii=False)
if __name__ == '__main__':
method = "post"
url = "//127.0.0.1:8000/api/departments/"
data = {
"data": [
{
"dep_id": "T02",
"dep_name": "接口测试学院",
"master_name": "Test-Master",
"slogan": "Here is Slogan"
}
]
}
res = SendMethod.send_method(method=method, url=url, json=data)
# print(res)
print(SendMethod.json_2_python(res))
# method = "get"
# params = {"$dep_id_list": "1, 2, 3"}
# res = SendMethod.send_method(method=method, url=url, json=data)
# print(SendMethod.json_2_python(res))
3、封装获取接口返回结果指定内容
该文件是封装处理返回值结果的一些方法。
我们需要用到一个Python中的模块JsonPath
,下面就先来介绍一下JsonPath
模块。
(1)JsonPath
介绍
用来解析多层嵌套的Json数据。
JsonPath
是一种信息抽取类库,是从JSON文档中抽取指定信息的工具,提供多种语言实现版本,包括:Javascript
,Python
, PHP
和 Java
。
JsonPath
对于 JSON 来说,相当于 XPath 对于 XML。
(2)JsonPath
安装
安装方法:pip install jsonpath
使用方法如下:
# 导入jsonpath模块
import jsonpath模块
# 嵌套n层也能取到所有key_nane信息,
# 其中:"$"表示最外层的{},
# ".."表示模糊匹配,
# 当传入不存在的key_nane时,程序会返回false。
res = jsonpath.jsonpath(response, f"$..{keyword}")[0]
"""
jsonpath方法说明
jsonpath(obj, expr, result_type='VALUE', debug=0, use_eval=True):
# obj表是要处理的json对象。
# expr是jsonpath匹配表达式。$..{keyword} 这种方式比较通用
"""
JsonPath
官方文档://goessner.net/articles/JsonPath
github上有它的应用://github.com/json-path/JsonPath(Java中的JsonPath
使用文档)
(3)JsonPath与XPath语法对比
Json结构清晰,可读性高,复杂度低,非常容易匹配,下表中对应了XPath的用法。
XPath | JSONPath | 描述 |
---|---|---|
/ |
$ |
根节点 |
. |
@ |
现行节点 |
/ |
. or[] |
取子节点 |
.. |
n/a | 取父节点,Jsonpath未支持 |
// |
.. |
就是不管位置,选择所有符合条件的条件 |
* |
* |
匹配所有元素节点 |
@ |
n/a | 根据属性访问,Json不支持,因为Json是个Key-value递归结构,不需要属性访问。 |
[] |
[] |
迭代器标示(可以在里边做简单的迭代操作,如数组下标,根据内容选值等) |
| | [,] |
支持迭代器中做多选。 |
[] |
?() |
支持过滤操作. |
n/a | () |
支持表达式计算 |
() |
n/a | 分组,JsonPath不支持 |
(4)getKeyword_forResult.py文件实现
"""
getKeyword_forResult.py文件说明:
1.作用
在接口返回值中,通过关键获取获取对应字段内容
2,前提:需要安装一个库:jsonpath库
安装jsonpath : pip install jsonpath
使用jsonpath模块进行处理更加方便
"""
# 导入jsonpath模块
import jsonpath
# 封装获取接口返回值方法
class GetKeyword:
# 定义成一个静态方法
@staticmethod
def get_keyword(response: dict, keyword):
"""
通过关键字获取对应返回值,如果有多个值,只返回第一个,
如果关键字不存在,返回False。
:param:response 数据源 字典格式
:param:keyword 要获取的字段
:return:
"""
try:
return jsonpath.jsonpath(response, f"$..{keyword}")[0]
except:
print("关键字不存在")
@staticmethod
def get_keywords(response: dict, keyword):
"""
通过关键字获取一组数据
:param response: 数据源 dict格式
:param keyword: 如果关键字不存在,返回False
:return:
"""
try:
return jsonpath.jsonpath(response, f"$..{keyword}")
except:
print("关键字不存在")
if __name__ == '__main__':
response = {
"count": 2,
"next": "下一页",
"previous": None,
"results": [
{
"dep_id": "10",
"dep_name": "tester_10",
"master_name": "master_10",
"slogan": "随便"
},
{
"dep_id": "11",
"dep_name": "tester_11",
"master_name": "master_11",
"slogan": "随便"
}
]
}
keyword = "dep_id"
# print(GetKeyword.get_keyword(response, keyword))
print(GetKeyword.get_keywords(response, keyword))
4、接口目录中的方法的实现
每一个接口或者一类接口封装成一个interface(也就是一个接口对应一个.py文件)
- 对该接口的请求:用于单接口测试。
- 根据业务获取接口返回值:用于关联接口测试。
(关于一个接口,所对应要测试哪几个方面的业务,都封装到该文件中,会用到上面commn目录中封装好的公共方法)
示例如下:
(1)示例1:封装新增学院接口
"""
新增学院接口
1.单接口测试方法
2.关联接口测试方法
获取返回值中的字段
"""
# 导入自定义的公共方法
from common.send_method import SendMethod
from common.getKeyword_forResult import GetKeyword
# 封装新增学院接口测试
class Add_department:
# url和请求方式对于一个接口来说是固定的,
# 所以这两个参数可以写在初始化方法中。
def __init__(self, url, method="post"):
self.method = method
self.url = url
def add_dep(self, data):
"""
定义新增学院接口:针对单接口测试
:param data: 新增学院的请求参数
:return:
"""
return SendMethod.send_method(self.method, url=self.url, json=data)
def get_keyword(self, data, keyword):
"""
获取新增成功后的关键字值:为关联接口测试准备
:param data:
:param keyword:
:return:
"""
res = self.add_dep(data)
# 获取新增学院接口返回值中的学院的具体某一属性
return GetKeyword.get_keyword(res, keyword)
if __name__ == '__main__':
url = "//127.0.0.1:8000/api/departments/"
data = {
"data": [
{
"dep_id": "T03",
"dep_name": "Test学院",
"master_name": "Test-Master",
"slogan": "Here is Slogan"
}
]
}
add = Add_department(url)
res = add.add_dep(data) # 新增学院接口方法
print(res)
keyword = "dep_id"
dep_id = add.get_keyword(data, keyword) # 获取新增成功后depid
print(dep_id)
(2)示例2:封装查询学院接口
"""
get_dep.py文件说明:
1.查询接口测试
2.获取查询接口返回值
"""
from common.send_method import SendMethod
class Get_Departments:
def __init__(self, url, method="get"):
self.url = url
self.method = method
def get_departments(self):
"""
查询所有学院
:return:
"""
return SendMethod.send_method(method=self.method, url=self.url)
def get_department(self, dep_id):
"""
根据id查询单个学院
:return:
"""
url = self.url + f"{dep_id}/"
return SendMethod.send_method(method=self.method, url=url)
def get_department_for_multpart(self, data):
"""
根据参数查询学院
:return:
"""
return SendMethod.send_method(method=self.method, url=self.url, parmas=data)
if __name__ == '__main__':
url_1 = "//127.0.0.1:8000/api/departments/"
data = {"$dep_id_list": "12,13"}
get_dep = Get_Departments(url=url_1)
# 查询所有学院
# print(get_dep.get_departments())
# 查询指定学院
dep_id = 16
# print(get_dep.get_department(dep_id))
# 根据条件查询学院
print(get_dep.get_department_for_multpart(data))
5、测试用例目录的实现
script
目录中存放的是测试用例,包括单接口和组合接口的测试用例。
测试用例是在unittest框架下编写,用法同UI测试框架。
(1)编写单接口测试用例
"""
测试新增学院接口
"""
# 测试用例是在unittest框架下编写
import unittest
from interface.add_departments import Add_department # 导入新增学院接口
from common.getKeyword_forResult import GetKeyword # 返回值处理接口
# 测试添加和查询学院的关联型接口
class Test_Add_Dep(unittest.TestCase):
def setUp(self) -> None:
self.url = "//127.0.0.1:8000/api/departments/"
# 实例化Add_department
self.add_dep = Add_department(self.url)
# 开始编写测试用例
def test_add_dep_success(self):
"""
测试添加学院成功接口
:return:
"""
# 封装请求参数
data = {
"data": [
{
"dep_id": "T100",
"dep_name": "Test学院",
"master_name": "Test-Master",
"slogan": "Here is Slogan"
}
]
}
# 新增学院
response = self.add_dep.add_dep(data)
# 获取添加成功后的dep.id
"""
# 因为直接使用该方法相当于又执行了一次添加学院接口
# 所以不能够这样调用
self.add_dep.get_depid(data)
"""
res_dep_id = GetKeyword.get_keyword(response["create_success"], "dep_id")
expect = "T100"
self.assertEqual(res_dep_id, expect)
# 测试添加学院完整性实现
def test_add_dep(self):
"""
测试添加学院接口
:return:
"""
# 封装请求参数
data = {
"data": [
{
"dep_id": "T101",
"dep_name": "Test学院",
"master_name": "Test-Master",
"slogan": "Here is Slogan"
}
]
}
# 新增学院
response = self.add_dep.add_dep(data)
"""
并返回值的验证有3种情况
#1.添加成功
#2.添川id已存在的学院
#3.参敖错误(自己实现)
根据对接口档的分析
可以通过判断返回值是否包含“status_code”区分1,2和3,然后区分1,2
根据返回值中already_exist.count是否为0,判断是否添加成功
"""
# 这里只判断上面的1,2情况,工作中根据实际业务自己在完成
if GetKeyword.get_keyword(response["already_exist"], "count") == 0:
# 获取添加成功后的dep.id
res_dep_id = GetKeyword.get_keyword(response["create_success"], "dep_id")
else:
res_dep_id = GetKeyword.get_keyword(response["already_exist"], "dep_id")
expect = "T101"
self.assertEqual(res_dep_id, expect)
if __name__ == '__main__':
unittest.main()
(2)编写组合接口测试用例
"""
测试新增和查询接口(组合接口业务)
先新增--->再查询
"""
# 测试用例是在unittest框架下编写
import unittest
from interface.add_departments import Add_department # 导入新增学院接口
from interface.get_departments import Get_Departments # 查询学院接口
from common.getKeyword_forResult import GetKeyword # 返回值处理接口
# 测试添加和查询学院的关联型接口
class Test_Add_Get_Dep(unittest.TestCase):
def setUp(self) -> None:
self.url = "//127.0.0.1:8000/api/departments/"
# 实例化Add_department添加学院
self.add_dep = Add_department(self.url)
# 实例化Get_Departments查询学院
self.get_dep = Get_Departments(self.url)
# 开始编写测试用例
def test_add_get(self):
# 封装请求参数
add_data = {
"data": [
{
"dep_id": "T03",
"dep_name": "Test学院",
"master_name": "Test-Master",
"slogan": "Here is Slogan"
}
]
}
# 一下逻辑待查证,知道组合的形式即可。
# 执行添加学院接口。目的:获取添加成功后的学院id
# 获取新增学院后的id
dep_id = self.add_dep.get_keyword(add_data, "dep_id")
# 查询新增学院信息
result = self.get_dep.get_department(dep_id)
# 通过获取查询后的学院id作为实际结果
res_dep_id = GetKeyword.get_keyword(result, "dep_id")
# 获取预期结果id
expect = GetKeyword.get_keyword(add_data, "dep_id")
# 断言结果
self.assertEqual(expect, res_dep_id)
if __name__ == '__main__':
unittest.main()
6、测试用例参数化实现
(1)准备数据
先创建一个Excel表格,里边填写如下数据
dep_id | dep_nane | master_nane | slogan | expect |
---|---|---|---|---|
T1001 | 学院1001 | tester_1001 | slogan1001 | T1001 |
学院1002 | tester_1002 | slogan1002 | 400 | |
T1003 | tester_1003 | slogan1003 | 400 | |
T1004 | 学院1004 | slogan1004 | 400 | |
T1005 | 学院1005 | tester_1005 | T1005 |
把Excel表格中的数据准备好之后,放入项目的data目录中即可。注意要把Excel表格存储为.xls
格式,兼容性好。
(2)在common目录中编写读取Excel数据的脚本
编写opreation_excel.py
脚本如下:
import xlrd
from xlrd import xldate_as_tuple
from datetime import datetime
class OperationExcel:
def __init__(self, filepath):
book = xlrd.open_workbook(filename=filepath)
self.sheet = book.sheet_by_index(0)
def read_excel(self):
rows = self.sheet.nrows
cols = self.sheet.ncols
all_data_list = []
for row in range(1, rows):
data_list = []
for col in range(cols):
ctype = self.sheet.cell(row, col).ctype
cell = self.sheet.cell_value(row, col)
if ctype == 2 and cell % 1 == 0:
cell = int(cell)
elif ctype == 3:
date = datetime(*xldate_as_tuple(cell, 0))
cell = date.strftime("%Y-%m-d %H-%M-%S")
elif ctype == 4:
cell = True if cell == 1 else False # 三目云算法
data_list.append(cell)
all_data_list.append(data_list)
return all_data_list
def get_data_by_dict(self):
keys = self.sheet.row_values(0)
values = self.read_excel()
data_list = []
for value in values:
tmp = zip(keys, value)
data_list.append(dict(tmp))
return data_list
if __name__ == '__main__':
oper = OperationExcel('testdata.xlsx')
# data = oper.read_excel()
data = oper.get_data_by_dict()
print(data)
(3)在script目录中编写测试用例
在script目录中编写test_add_dep_batch.py
测试用例。
"""
新增学院接口测试--批量新增
"""
# 测试用例是在unittest框架下编写
import unittest
from interface.add_departments import Add_department # 导入新增学院接口
from common.getKeyword_forResult import GetKeyword # 返回值处理接口
# 步骤1:导入OperationExcel数据读取脚本和ddt模块
from common.opreationexcel import OperationExcel
import ddt
# 步骤2:对OperationExcel进行实例化
# 获得文件对象
oper = OperationExcel("../data/add_dep.xls")
# 获取数据
test_data = oper.get_data_by_dict()
# 测试添加和查询学院的关联型接口
# 步骤3
@ddt.ddt()
class Test_Add_Dep(unittest.TestCase):
def setUp(self) -> None:
self.url = "//127.0.0.1:8000/api/departments/"
# 实例化Add_department
self.add_dep = Add_department(self.url)
# 开始编写测试用例
# 步骤4
@ddt.data(*test_data)
def test_add_dep_success(self, data): # 步骤5:出入data参数
"""
测试添加学院成功接口
:return:
"""
# 步骤6:准备数据
req_data = {
"data": [
{
"dep_id": data["dep_id"],
"dep_name": data["dep_name"],
"master_name": data["master_name"],
"slogan": data["slogan"]
}
]
}
# 新增学院
response = self.add_dep.add_dep(req_data)
# 获取添加成功后的dep.id
# 步骤7:完成测试逻辑
# 如果添加学院参数请求错误,会出现status_code属性
# 且status_code属性返回400
if "status_code" in response.keys():
res = GetKeyword.get_keyword(response, "status_code")
else:
# 添加学院成功,则获取添加后学院的id
"""
# 因为直接使用该方法相当于又执行了一次添加学院接口
# 所以不能够这样调用
self.add_dep.get_depid(data)
"""
res = GetKeyword.get_keyword(response["create_success"], "dep_id")
# 断言
self.assertEqual(res, data["expect"])
if __name__ == '__main__':
unittest.main()
以上就完成了一个最简单,最基础的接口自动化测试框架的搭建。