关键字 开发-13 API与用例分层

发布时间 2023-12-20 16:08:41作者: dack_deng

前言

前面我们都是在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个步骤。