大家好~我是
米洛
!
我在从0到1打造一个开源平台, 也在编写一套完整的接口测试平台系列教程
,希望大家能够多多支持。
欢迎关注我的公众号米洛的测开日记
,获取最新文章教程!
# 回顾
上节内容我们编写好了数据驱动相关
的接口,但却有很多困扰
随之而来。接着就来说说我们提出这些困扰,并解决之。
# 问题汇总
由于数据驱动
的改造,可能会引发以下几个问题:
原先的执行用例的方式需要变化
数据驱动以后,我们批量执行case的时候需要带上环境字段了,否则
数据驱动
没法使用。原先能够直接调试case,也就是单条case在线执行
现在由于环境的关系+多数据的情况,我们只能把这块进行调整,让单个case的调试变为
单条测试数据
的调试。
执行测试用例逻辑的变化
之前我们执行测试用例的时候,举个例子:
登录接口
,我们是在前置条件里面写好了具体的登录用户名和密码。但在数据驱动的情况下,又不一样了。我们得把用例里面变化的数据全部
抽离出来
,其实这么做的目的是为了让数据更灵活,也为了以后能支持数据工厂
。所以这就会导致我们执行case的整套逻辑会发生
剧烈变化
,让人头大!!!断言逻辑的改造
细心的朋友可能会发现,断言部分之前没有很好地支持
变量
,但现在不一样,我们要根据不同的参数完成不同的断言操作,所以我们也得支持。
# 迎接挑战
虽然这些挑战比较麻烦,但好在大多
是体力活儿。因为以前的逻辑和现在逻辑的区别引发,只要耐心,就能逐一击破。而且之前埋下的坑比如断言变量
没替换等等都可以随之解决。
# 改造报告
PityTestResult新增字段
之前我们编写报告的时候,都是一条case一条数据,那现在有了数据驱动,我们的报告就需要做出一定的
改变
。现在我们需要把数据的标识和数据的内容都
写入报告
,这样的话在查看报告的时候就能清楚地看到每条数据的执行结果,可以对号入座
。PityTestResult初始化方法新增字段
- 同理,插入测试结果的时候也要调整
- 查询报告的时候也要相继调整
# 改造用例执行器
我们之前的执行器有许多不完善的地方,比如不支持断言里面的数据变化,再者只替换了一次数据,而现在我们要做的是什么呢?
每当有新的变量生成,我们都要去寻找变量去替换,这样做虽然显得工作量很重复,但是完整地保证了生成的变量不会漏掉。
- 编写replace_args方法
因为没法度量
变量什么时候生成,所以要做的就是每当有新的变量产生就替换变量
。
- replace方法
可以看到他们都调用了核心方法: replace_cls,也就是替换一个对象,通过属性+值的形式。
我们来看一下replace_cls方法
剖析一下: 遍历参数字典,接着去指定的对象字段
里面查询是否有这个变量,什么意思呢?假设有这样一个对象:
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
2
3
4
5
假设此时学生的name和age分别是${student_name}
和18
。
我们这边有个参数字典为:
params = {"student_name": "miro"}
很显然,我们要做的效果就是:
- 遍历params,此时key=student_name, value=miro
- 根据指定的字段:
name
和age
(注意是个列表) - 先找name,发现name的值是
${student_name}
(我们pity内部约定的变量形式),和student_name吻合,所以我们要把${student_name} 替换为miro
- 调用setattr方法,把student_name替换成它应该变成的值: miro
- 由于age字段的值是18,并不是一个变量,所以循环结束
就是上面这个思路,不知道大家有没有看明白。
- replace_params
这块是变量替换的精髓
所在,也是最难弄懂的地方。
这个方法具体的处理逻辑就是从JSON字符串中找到对应的值,并能支持字段取值。
比如a = '{"human": {"age": 1}}'
这个json字符串,如果我们要取到age,我们需要这么写${res.human.age}
.
这个方法做到的事情就是,从res获取human,接着获取age,最终返回一个字段,用于替换:
new_data = {"human.age": 1}
完成上述操作,接着就能够从字符串中拿到human.age的值(1)了,从而能够将所有用例产生的变量
分配到其他case或者其他数据。
这块之所以有一些改动
,是因为我们把response里面的所有数据都改成了字符串,以前比如response可能是字典
。
# 改造数据构造器
# run方法改造
async def run(self, env: int, case_id: int, params_pool: dict = None, request_param: dict = None, path="主case"):
"""
开始执行测试用例
"""
response_info = dict()
# 初始化case全局变量, 只存在于case生命周期 注意 它与全局变量不是一套逻辑
case_params = params_pool
if case_params is None:
case_params = dict()
req_params = request_param
if req_params is None:
req_params = dict()
try:
case_info, err = await TestCaseDao.async_query_test_case(case_id)
if err:
return response_info, err
response_info["case_name"] = case_info.name
method = case_info.request_method.upper()
response_info["request_method"] = method
# Step1: 替换全局变量
await self.parse_gconfig(case_info, *Executor.fields)
self.append("解析全局变量", True)
# Step2: 获取构造数据
constructors = await self.get_constructor(case_id)
# Step3: 获取断言
asserts, err = await TestCaseAssertsDao.async_list_test_case_asserts(case_id)
if err:
return response_info, err
# Step4: 替换参数
self.replace_args(req_params, case_info, constructors, asserts)
# Step5: 执行构造方法
await self.execute_constructors(env, path, case_info, case_params, req_params, constructors, asserts)
response_info["url"] = case_info.url
# Step6: 获取后置操作
# TODO
# Step7: 批量改写主方法参数
await self.parse_params(case_info, case_params)
if case_info.request_headers != "":
headers = json.loads(case_info.request_headers)
else:
headers = dict()
if "Content-Type" not in headers:
headers['Content-Type'] = "application/json; charset=UTF-8"
if case_info.body != '':
body = case_info.body
else:
body = None
# Step5: 替换请求参数
body = self.replace_body(request_param, body)
# Step6: 完成http请求
if "form" not in headers['Content-Type']:
request_obj = AsyncRequest(case_info.url, headers=headers,
data=body.encode() if body is not None else body)
else:
if body is not None:
body = json.loads(body)
request_obj = AsyncRequest(case_info.url, headers=headers, data=body)
res = await request_obj.invoke(method)
self.append(f"http请求过程\n\nRequest Method: {case_info.request_method}\n\n"
f"Request Headers:\n{headers}\n\nUrl: {case_info.url}"
f"\n\nBody:\n{body}\n\nResponse:\n{res.get('response', '未获取到返回值')}")
response_info.update(res)
# 执行完成进行断言
asserts, ans = self.my_assert(asserts, response_info)
response_info["asserts"] = asserts
# 日志输出, 如果不是开头用例则不记录
if self._main:
response_info["logs"] = self.logger.join()
response_info["status"] = ans
return response_info, None
except Exception as e:
Executor.log.error(f"执行用例失败: {str(e)}")
self.append(f"执行用例失败: {str(e)}")
if self._main:
response_info["logs"] = self.logger.join()
return response_info, f"执行用例失败: {str(e)}"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
方法比较长,我就直接贴代码了。改造点主要有以下几处:
- 新增env参数
- replace_args替换掉replace_params
- 打印完整的http请求
- 在执行异常的时候,返回尽可能多的信息
# 改造执行方法
在执行之前呢,先获取这个case在对应环境的测试数据,然后将数据解析为json.
接着就是用异步的方式,挨个儿执行。
其他的比如批量执行,也和这个细节差不多,因为篇幅的问题就不讲了。
大家有兴趣可以看看request/http.py文件。