有个人说过,自动化测试能让不懂代码的人只需要写测试case(数据)就能实现测试,我觉得他说的dei
case使用封装
数据case
最终将只需要维护这个数据文件即可实现对100个接口的1000case(愿景)。
# data/cases/case_api.yaml
- name: friend_add
url: http://127.0.0.1:5000//friend/add
method: post
request:
json:
uname: name_add_friend_${timestamp()}
sex: 0
validate:
eq:
- { kw: "$.status_code[0]", expected: 200 }
- { kw: "$.code[0]", expected: 0 }
in:
- { kw: "$.message[0]", expected: 添加成功}
yaml读html获取case参数
# util/yaml_util.py
def getCases(path):
with open(path) as f:
yy = yaml.safe_load(f)
return yy if yy else dict()
testcase代码
# testcases/testapi.py
from utils.requests_util import RequestsUtil
from utils.yaml_util import getCases
from utils.assert_util import AssertUtil
class TestApi:
@pytest.mark.parametrize('case', getCases('data/cases/case_api.yaml')) # 在这里读文件传测试数据
def testApi(self, case):
logging.info('=============== Start do case: {}'.format(case['name']))
url = case['url']
ret_json = RequestsUtil().request(case['method'], url, **case['request']) # 调用封装的request
AssertUtil().assert_resp(case['validate'], ret_json) # 调用封装的assert
动态变量
是为了这种场景:比如新增数据的接口,要求uname不能重复,怎么让它每天的参数传的不一样呢。用代码当然可以实现,这里就是实现了在case文件中(yaml)也能使用这些函数。我用了shell中取变量的语法来写,大概就是${python调用}
这样。然后在执行case的时候,去把这中语法解析出来。这种类似反射的调用方式是不能被编译器的检查语法识别的。就是说在pycharm里,我即使没有写timestamp()的实现,它也不会包语法错误,但是执行的时候,就会报NameError: name 'xxxx' is not defined
。就是说要保证你写的函数跟evalArgs()
在同一个模块中,或者从其他地方导入进来。
函数写在了utils/__init__.py
中,timestamp()的功能比较简单,就是拿到当前的时间戳。evalArgs通过正则来解析case中的${python调用}
,把它们替换成调用函数的结果。
# utils/__init__.py
def evalArgs(data): # 解析参数中的动态参数/函数
if type(data)==list:
for i, di in enumerate(data):
data[i] = evalArgs(di)
if type(data)==dict:
for k, v in data.items():
data[k] = evalArgs(v)
if type(data)==str:
reg = re.compile(r'.*?(\$\{(.+?)\}.*?)+') # 解析这样的格式 ${timestamp()}
res = reg.findall(data)
if res:
for ri in res:
data = data.replace(ri[0], eval(ri[1])) # 通过eval执行其中的函数
# 注意:要保证被eval执行的函数在当前作用域内存在(或者定义在当前文件,或者导入到当前文件)
return data
return data
def timestamp():
return time.strftime('%Y%m%d%H%M%S')
断言封装
就是这里
这里通过jsonpath的语法来定位返回结果中的元素
# utils/assert_util.py
import logging
import jsonpath
class AssertUtil:
def assert_resp(self, validate, ret_json):
for vtype, values in validate.items():
for vi in values:
logging.info('Do assert, with msg: {}: {}'.format(vtype, vi))
# 通过jsonpath从响应结果中提取数据
if vi['kw'].endswith('[0]'): # emm,非列表元素不能通过在keyword中的[0]提取
actual = jsonpath.jsonpath(ret_json, vi['kw'][:-3])[0]
else:
actual = jsonpath.jsonpath(ret_json, vi['kw'])
expected = vi['expected']
cmd = 'self.assert_{}(actual, expected)'.format(vtype)
try:
eval(cmd)
except NameError as e:
assert False, 'Exception in do assert, Not support assert type: {}, with msg: {}'.format(vtype, e)
def assert_in(self, actual, expected):
assert expected in actual, 'assert_in error, (expected){} not in (actual){}'.format(expected, actual)
# 我这里是定义的 expected in actual,视个人需求可修改
def assert_eq(self, actual, expected):
assert actual == expected, 'assert_eq error, (expected){} != (actual){}'.format(expected, actual)
前面我写过做requests的封装,这里为了能同时对response属性和后端返回的数据进行断言,我在封装requests的代码里把它们放在了一个字典中。
# utils/requests_util.py
ATTRS_RESP2RET = ['status_code', 'headers']
...
ret = {key: getattr(resp, key, '') for key in ATTRS_RESP2RET}
try:
ret_json = resp.json()
except Exception as e:
self.logger.error('Exception in dump resp to json, with resp.text: {}'.format(resp.text))
raise e
return {**ret, **ret_json}
执行测试
这里用flask简单写了个被测服务,启动该http服务
$ python mockserver.py
执行自动化测试
$ pytest testcases/testapi.py -vs