接口自动化非常简单,大致分为以下几步:
- 准备入参
- 调用接口
- 拿到2中response,继续组装入参,调用下一个接口
- 重复步骤3
- 校验结果是否符合预期
一个优秀接口自动化框架的特点:
- 【编码门槛低】,又【能让新手学到技术】
- 【低调试门槛】
- 【优秀的可读性】
- 【很好的可维护性】
- 能很【方便多人协同】,以便【自动化代码能不断积累】形成规模效应,且【自动化case覆盖率可量化】
- 有失败重试机制,克服环境的不稳定
- 【清晰的测试报告】
- 能【低成本且灵活地触发】(指定环境、指定时间、指定范围)
综上,我们需要搭建一个同时满足以上特点的轻量级接口自动化框架
首先,管理接口
达成:【编码门槛低】【方便多人协同】【优秀的可读性】【很好的可维护性】
- 被测接口单独管理,目录结构和和开发使用的yapi保持一致,统一放置在仓库下,“接口管理”目录中;如下:
- 每个接口使用yaml文件管理,如下,且使用中文命名增加可读性。
-
method: post host: "${host}" url: /copy/trading/v1/follower/edit headers: {token: "799b72f3f94973f5a54a54204eac96a1aa94cd5d365245929098e921b2ddc154"} body: { "followType": 1, "traderUid": 102806, "margin": "100", "followAmount": "1000", "stopLossRation": null, "positionMode": 1, "positionModeConfig": 0, "leverModeConfig": 0, "leverage": 1, "marginModeConfig": 0, "marginMode": 0, "positionRisk": null, "slippage": "0.006", } response: code: # 响应码(0: 成功) msg: # 返回描述(code为0时,返回:成功) data: # 响应数据
- 调用接口时,默认用yaml文件中的参数(剔除response,response只做示例用),如需替换,会在调用过程中,标记具体需要替换的key value即可,如下,方便快捷
-
r.invoke_api("更新用户配置.yaml", headers={"token": 'trader_son_token'}, body={"positionMode": 1})
其次,编写代码,调用接口
达成:【编码门槛低】【优秀的可读性】【低调试门槛】【清晰的测试报告】
- 封装requests包,使用requests.session(),根据上述接口文档,调用get或post方法,且使用allure将入参和返回都放置到测试报告里面,且此块代码无需实际接口测试同学关注。【能让新手学到技术】
-
class RequestUtil: def invoke_api(self, api, headers=None, body=None): """ 解析需要调用的接口,并且进行参数替换,基础断言等 :param api: 需要调用的接口 :param header: 需要传入的请求头 :param body: 需要传入的请求体 :return: 返回接口响应的结果 """ api_name = api param = YamlUtil.parse_api(api_name) if headers is not None: for key in headers: param['headers'][key] = headers[key] if body is not None: for key in body: param['body'][key] = body[key] params = self.replace_value(param) with allure.step(f"步骤:{api_name}"): response = self.send_request(params) if self.run_mode == 1: print("") print(f"调用接口:{api_name}") print(params) print(response) return response else: return response def send_request(self, *args): """ 发起接口请求 :param args: 接口请求的参数 :return: 接口返回的数据 """ method = args[0]['method'].lower() ip = self.replace_value(args[0]['host']) url = ip + args[0]['url'] headers = args[0]['headers'] body = args[0]['body'] data = body if method == "get": res = self.sess.get(url, params=data, headers=headers) elif method == "post": if 'files' in args: files = args[0]['files'] file_path = os.path.join(YamlUtil.data_file_path, files) with open(file_path, 'rb') as file: files = {'file': ('image.jpg', file)} res = self.sess.post(url, files=files, headers=headers) res = self.sess.post(url, json=data, headers=headers) else: print("请求方式错误") try: res = res.json() with allure.step(f"URL:{url}"): ... with allure.step(f"Method:{method}"): ... with allure.step(f"Headers:{json.dumps(headers)}"): ... with allure.step(f"Body:{json.dumps(data, ensure_ascii=False)}"): ... with allure.step(f"Response:{json.dumps(res, ensure_ascii=False)}"): return res except Exception as e: print("该接口返回的结果不是json数据?") res = res.text with allure.step(f"URL:{url}"): ... with allure.step(f"Method:{method}"): ... with allure.step(f"Headers:{json.dumps(headers)}"): ... with allure.step(f"Body:{json.dumps(data, ensure_ascii=False)}"): ... with allure.step(f"Response:{json.dumps(res, ensure_ascii=False)}"): return res
- 通过pytest的fixture装饰器,将RequestUtil注入进去每个测试用例里面去
-
@pytest.fixture(scope="session") def r(): return RequestUtil()
- 测试编码同学只需要使用方法,即可完成接口调用和response获取
- 接口:百度.yaml
-
method: post host: https://ug.baidu.com url: /mcp/pc/pcsearch headers: {} body: { "errno": 0, "errmsg": "ok", "data": { "log_id": "1652589279", "action_rule": { "pos_1": [], "pos_2": [], "pos_3": [] } } }
事实上,测试代码只有这么点:【编码门槛低】只需要能看懂接口文档,和http的基本知识,以及知道Python的字典和assert,便可轻松完成编码
def test_百度(r):#其中 r为fixture装饰器,通过conftest.py注入进去 resdic= r.invoke_api("百度.yaml", headers={}, body={}) assert resdic[errmsg] == "ok" assert resdic[data][log_id] == "3277317105"
- 不管成功失败,测试报告都会打出每一次请求的入参和返回,如下图,都不用在IDE里面调试。【低调试门槛】【清晰的测试报告】
- 另外,数据驱动的case,用如下的方式编写
-
import pytest @pytest.mark.parametrize('emails,password,assert1', [ ('11111@qq.com', 'Tset111', 'success'), ('22222@qq.com', 'Tset222', 'fail'), ('33333@qq.com', 'Tset333', 'error'), ]) def test_注册用户(r, emails, password, assert1): response_info = r.invoke_api('注册用户.yaml', body={"emails": emails, "password": password}) assert response_info['data'] == assert1
再次,测试用例的组织和结构
达成:【方便多人协同】【自动化代码能不断积累】【自动化case覆盖率可量化】
- 手工用例遵循实际的业务模块树形结构,做较精细的模块拆分
- 末级手工用例使用xmind管理,作为用例树的末级,标题上按如下打标:哪些场景需要被覆盖(目标)?已被覆盖(进度)?功能覆盖率多少(量化)?哪些地方还需要验证前端?一目了然,工时评估更透明
- 是否需要自动化
- 是否已被自动化
- 是否还需要验证前端
- 自动化测试用例目录,和手工用例目录保持一致,
- 每个自动化case一个.py文件,自动化测试用例名与手工测试名保持一致。如此保障一一对应
- 如若涉及一对多:
- 如果涉及单接口的,自动化测试用例名与手工用例末级目录一致;
- 如果涉及数据驱动的,自动化测试用例名与手工用例末级目录一致;
- 通过以上措施,很方便计算出自动化case覆盖率(分子为已完成自动化case,分母为达标为:需要自动化的case数)
再次,通过配置文件,和参数化运行
达成【低成本且灵活地触发】
- yaml方式的配置文件
-
run_mode: 2 # 1调试模式/2正式模式 run_env: one # 运行脚本环境 test: host: http://api.test.xyz db: host: port: user: password: redis: host: password: testa: host: http://api.testa.xyz db: host: port: user: password: redis: host: password: # 环境 one: # 接口地址 host: # b连接信息 db: host: port: user: password: # redis连接信息 redis: host: password:
- main函数运行的时候,拿到具体运行的环境信息,读取对应的配置
-
import os import pytest from pytest_jsonreport.plugin import JSONReport import argparse from Core.parse_yaml import YamlUtil from Core.robot_utils import Robot def main(env): YamlUtil.clear_env_yaml() if env.env is not None: e = env.env data = {"env": e} YamlUtil.write_env_yaml(data) current_path = os.path.dirname(os.path.abspath(__file__)) json_report_path = os.path.join(current_path, 'report/json') html_report_path = os.path.join(current_path, 'report/html') plugin = JSONReport() pytest.main(["自动化用例/百度搜索.py", '--alluredir=%s' % json_report_path, '--clean-alluredir'], plugins=[plugin]) # 生成allure报告 os.system('allure generate %s -o %s --clean' % (json_report_path, html_report_path)) os.system('allure open %s' % html_report_path) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--env", help="参数为需要执行的环境") args = parser.parse_args() main(args)
-
#在testa环境运行 python run.py --env testa