前言
前面我们都是在yml文件中写单个用例的去调用,如果后期涉及到业务流程的时候,单个用例就无法满足需要测试的业务流程。如何实现这个功能,我们可以将用例和api进行分离,api层只写单个接口的数据,然后在用例层处理业务流程,不断的调用api的接口,从而可以满足我们的需求。那么这篇将讲如何在yml文件中实现接口业务流程的测试。
1. API层
我们创建api文件夹,存放api的接口信息,创建testcase文件夹,存放test业务用例,实现api和用例的分离。
1.api层:描述接口 request 请求,可以带上 validate 基本的校验
2.testcase用例层: 用例层调用多个api并通过顺序引用,实现业务流程
如图可以清楚的知道实现的原理,不同的testcase中可以调用多个api,实现用例业务流程。
# api/login.yml
name: 登录api
request:
url: /api/v1/auth/login
method: POST
json:
username: ${username}
password: ${password}
extract:
code1: $.code
code2: body.code
token: $.data.token
msg: '"message":"(.*?)"'
validate:
- eq: [$.code, "000000"]
- eq: [$.message, OK]
api层的login.yml文件,可以实现接口的信息,请求,以及可以提取接口返回的值,并也支持接口的校验,同时如果需要引用变量,也可以支持jinja2语法。比如登录用户名密码在不同用例中会用到不同的账号,那么可以使用变量\({username},\){password}。需注意的是,API层不支持单独运行,因为它只是记录用例的接口信息部分,不能当成用例去执行,用例执行需使用 test_*.yml命名。
2.testcase层
用例层通过api关键字导入需要的API,导入的路径是相对路径,需根据项目的根目录去导入。如果执行过程中代码无法识别哪个是项目根目录,最好在项目的根目录下放pytest.ini 文件,pytest会以pytest.ini文件所在的目录为项目根目录。
项目的目录如下所示:
├─api └─ login.yml ├─testcase └─ test_login.yml └─conftest.py └─pytest.ini
所以不管用例文件test_*.yml在哪个目录,都是以项目根目录去导入API的yaml文件。如下登录用例层:在config中可以传入请求时需要的参数,teststeps表示用例的执行步骤,通过-分割步骤1和步骤2。例子中实现了登录-步骤1以及登录-步骤2,2个步骤都是从login.yml中获取到api的信息,从而实现多个步骤的业务流程用例。
# testcase/test_login.yml
config:
name: 登录用例
variables:
username: "admin"
password: "Admin@22"
teststeps:
-
name: 登录-步骤1
api: api/login.yml
extract:
code1: $.code
code2: body.code
token: $.data.token
validate:
- eq: [$.code, "000000"]
- eq: [status_code, 200]
-
name: 登录-步骤2
api: api/login.yml
validate:
- eq: [ status_code, 200 ]
3.API和用例分层功能实现
如下所示的实现功能的代码块,对代码块进行讲解:
1.从def execute_yaml_case用例函数这里开始,这是实现每个用例的testcae函数
2.上面已经将收集到的全部test_*.yml用例放入case中,于是用例中通过case[call_function_name],拿到每个testcase,如:{"teststeps",{[...],[...]}}
3.teststeps有多个步骤,使用for循环获取多个步骤,分别请求处理:for step in case[call_function_name]:
4.再通过for item, value in step.items():处理每个步骤总的关键字参数
5.判断elif item == 'api':处理api:1-实现通过相对路径获取到API层的api信息,2-深拷贝一份新的request,单独处理request请求,3-渲染request的变量,4-单独封装run_request请求。
case = {} # 收集用例名称和执行内容
......
def execute_yaml_case(args):
"""执行yaml 中用例部分,根据这个函数动态生成其他测试用例函数"""
log.info(f"执行的参数: {args}")
# 更新fixtures的返回值,到容器中
self.context.update(args)
# 被谁调用
call_function_name = inspect.getframeinfo(inspect.currentframe().f_back)[2]
log.info(f'执行的内容: {case[call_function_name]}')
for step in case[call_function_name]:
step_context = self.context.copy()
step_name = step.get('name')
if step_name:
log.info(f'用例执行:{step_name}')
step_name = render_template_obj.rend_template_any(step_name, **step_context)
if 'validate' not in step.keys():
step['validate'] = []
for item, value in step.items():
# 根据关键字去执行
if item == 'name':
pass
elif item == 'print':
log.info(value)
elif item == 'api':
# 通过hook函数:内置request获取到项目的根路径
root_dir = args.get('request').config.rootdir
# 获取文件路径
api_path = Path(root_dir).joinpath(value)
raw_api = yaml.safe_load(api_path.open(encoding='utf-8'))
api_validate = raw_api.get('validate', [])
copy_value = copy.deepcopy(raw_api.get('request')) # 深拷贝一份新的value
# 渲染变量
copy_value = render_template_obj.rend_template_any(copy_value, **self.context)
BASE_URL_api = base_url if base_url else args.get('base_url')
response = self.run_request(args, copy_value, BASE_URL_api, context=step_context)
......
def run_request(self, args, copy_value, base_url, context=None):
"""运行request请求"""
request_session = args.get('requests_function') or args.get('requests_module') or args.get('requests_session')
# 加载参数化的值和fixture的值
if context is None:
request_value = render_template_obj.rend_template_any(copy_value, **self.context)
else:
request_value = render_template_obj.rend_template_any(copy_value, **context)
response = request_session.send_request(base_url=base_url, **request_value)
return response
根目录下运行:pytest testcase
,如图所示,运行成功,登录用例包含了2个步骤。