Python+Pytest+tox 接口自动化测试框架
该接口自动化测试框架使用python编写,同时使用tox配置不同的测试环境,以及使用pytest管理测试用例。使配置测试环境、维护测试数据、取接口信息和处理接口依赖等更加灵活
目录
前言:
1. 该框架重构自之前的框架
Python+Requests+Pytest 接口自动化测试脚本总结
2. 目录截图
一、tox简介和使用介绍
1. tox简介
tox是一个管理测试虚拟环境的命令行工具,其核心作用是:
支持创建隔离的python环境,在里面可以安装不同版本的python解释器和各种依赖库,方便开发者做自动化测试、打包和持续集成等
2. 配置文件tox.ini
[tox]
skipsdist=True
envlist = py37-int
[testenv]
basepython = python3.7
setenv =
ENV = {env:ENV:sandbox}
PYTHONIOENCODING = utf-8
commands =
# tox will not install new packages when requirements changing unless "tox -r" recreate env
pip install -q -i https://mirrors.aliyun.com/pypi/simple/ -r {toxinidir}/requirements.txt
# Append more arguments inside "pytest" sections with "addopts"
pytest --alluredir ./allure-results {posargs}
1)[tox]下面是全局性的配置项:
"skipsdist"是指定tox在运行过程中跳过打包环节,因为项目无打包需求;
“envlist”字段定义了tox操作的环境
2)[testenv]下面是虚拟环境默认配置项:
“basepython”配置的是python解释器版本;
“setenv”设置环境变量,在项目中可以读取环境变量,从而决定运行哪个环境的配置
3)取操作系统的环境变量:
{env:ENV:sandbox},效果等同于os.environ['ENV']。在取不到环境变量时则取配置的默认值”sandbox“。该值是测试环境,比如prod代表生产环境,sandbox代表测试环境。那么配置环境变量时,可以配置”sandbox“或者”prod“作为默认值;
PYTHONIOENCODING = utf-8,配置编码方式为utf-8,防止中文乱码
4)执行命令:
“commands”后面配置构建好虚拟环境后要执行的命令。比如安装插件和生成allure的html测试报告
二、框架搭建
1. 封装接口请求
1)导入requests库,封装requests库的get()和post()方法
2)call()函数中,封装了get()和post()函数;参数多了一个method,且参数params和headers均可以不传参
3)使用raise主动抛出异常,如果接口方法不是get或者post,则抛出错误信息
# -- coding: utf-8 --
import requests
def call(url, method, params=None, headers=None):
method = method.lower()
if method not in ('get', 'post'):
raise Exception('Invalid request method [%s], should be "get" or "post"' % method)
if method == 'get':
return get(url, params, headers)
elif method == 'post':
return post(url, params, headers)
else:
raise Exception('Should not run into here')
def get(url, params, headers=None):
resp = requests.get(url, params=params, headers=headers)
return resp
def post(url, params, headers=None):
resp = requests.post(url, data=params, headers=headers)
return resp
2. 配置接口信息
1)接口信息配置在yaml文件中。常规的接口信息数据项介绍:
financial_invest_demand: # 接口名称
description: 购买产品 # 接口描述
depend_on: financial_active_list # 接口依赖,无依赖的接口不需要该项,被依赖的接口该项值为空
category: financial # 接口参数所在路径,参数文件命名为”接口名称.json“
method: post # 接口类型
url: /invest/ # 接口url
params: # 接口参数,也可直接维护在json文件中
product_id:
2)skip-test: true # 执行测试用例时,跳过该用例
3)enabled_only: sandbox # 区分测试环境
4)disable_auth: true # 区分是否是登录接口
5)&和*:&用来建立锚点,*用来引用锚点。
比如”&login_sandbox“就是建立一个sandbox环境登录的接口信息锚点,然后”environments“中使用”*login_sandbox“就可以直接引用”bootstrap_sandbox“中的所有数据项
6)如何自动生成接口信息?
可以使用抓包工具fiddler或者charles抓包后,生成.har文件。然后通过httprunner的har2case,将.har文件转换成yaml格式
default_params: &default_params
skip_test: true
device_id: XXX
app-version: 4.26.0
bootstrap_sandbox: &login_sandbox
description: 【登录】手机号
skip_test: true
enabled_only: sandbox
category: account
method: post
url: /login/
disable_auth: true
params:
<< : *default_params
environments:
sandbox:
endpoint: https://XXX.com
bootstrap:
<< : *login_sandbox
prod:
endpoint: https://XXX.com
bootstrap:
<< : *login_prod
headers:
Authorization: Basic XXX
stage:
endpoint: https://XXX.cn
login_with_mobile:
<< : *login_sandbox
financial_main:
description: 首页
method: get
url: /main/
financial_invest:
description: 购买产品
depend_on: financial_active_list
category: financial
method: post
url: /invest/
params:
product_id:1
sort_order: -1
limit:
financial_active_list:
description: 产品列表
depend_on:
category: financial
method: get
url: /list/
3. 维护测试数据
1)某些get和post接口中,需要params参数。可以把params维护在yaml配置文件中,也可以单独维护在json文件中
2)每个需要参数的接口维护一个json文件。测试数据存在list里,且以dict形式存储。一个list里可以有多个dict,即可以有多组测试数据。比如登录接口,可以维护两组数据:邮箱账号和手机账号
[
{
"params": {
"product_id": "1",
"sort_order": -1,
"limit": ""
}
}
]
3)json文件路径,fixtures+测试环境+category(接口分类)+接口名称.json
举例说明:fixtures+sandbox+financial+financial_invest_demand.json
4. 配置日志
1)使用python中的 logging.config.dictConfig 函数配置日志
2)使用命令:tox > log_name.log 把测试用例的运行结果,输出到日志文件中
import logging.config
logging.config.dictConfig({
"version": 1,
"disable_existing_loggers": False,
"root": {"level": "DEBUG", "handlers": ["console"]},
"formatters": {
"verbose": {
"format": "%(levelname)s %(asctime)s %(pathname)s#%(lineno)d:\n %(message)s"
},
"concise": {"format": "%(levelname)s %(asctime)s %(message)s"},
"lean": {"format": "%(asctime)s: %(message)s"},
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "lean",
},
},
}
)
5. 加载接口配置信息
1)在函数 _load_api_dsl() 内定义三个全局变量,_env, _api_descriptor, _all_testcases
2)_env 是个字典(dict),存储metadata/api.yml中配置的测试环境所对应的登录接口信息;
_api_descriptor 是个dict,存储api.yml中接口信息,除了跳过(skip_test=true)、带有依赖(存在depend_on)和环境配置(environments)的接口信息;
_all_testcases 是个元组(tuple),里面嵌套tuple,存储接口名称(api_name)和描述(description)
_env = None
_api_descriptor = None
_all_testcases = None
# 加载接口配置信息(metadata/api.yml)
def _load_api_dsl(env):
global _env, _api_descriptor, _all_testcases
api_dsl = os.path.join('metadata', 'api.yml')
with open(api_dsl, 'r', encoding='utf-8') as f:
dsl = yaml.full_load(f) # dsl是dict类型,存储的是api.yml中所有的接口信息
_env = dsl['environments'][env] # 运行环境相对应的登录接口信息
del dsl['environments']
_api_descriptor = dict() # 存储api.yml中接口信息
testcases = list() # 存储接口名称(api_name)和描述(description)
for key, api_dsl in dsl.items():
if ('skip_test' in api_dsl and api_dsl['skip_test']) or ('depend_on' in api_dsl):
continue
desc = api_dsl['description'] if 'description' in api_dsl else ''
testcases.append((key, desc, ),)
_api_descriptor[key] = api_dsl
_all_testcases = tuple(testcases)
6. 加载有接口依赖的接口配置信息
1)在函数 _load_api_dsl_depend() 内定义三个全局变量,_env, _api_descriptor, _all_testcases
2)_env 是个字典(dict),存储metadata/api.yml中配置的测试环境所对应的登录接口信息;
_api_descriptor_depend 是个dict,存储带有依赖(存在depend_on)的接口信息;
_all_testcases_depend 是个元组(tuple),里面嵌套tuple,存储带有依赖的接口名称(api_name)和描述(description)
def _load_api_dsl_depend(env):
global _env, _api_descriptor_depend, _all_testcases_depend
api_dsl = os.path.join('metadata', 'api.yml')
with open(api_dsl, 'r', encoding='utf-8') as f:
dsl = yaml.full_load(f) # dsl是dict类型,存储的是api.yml中所有的接口信息
_env = dsl['environments'][env] # 运行环境相对应的登录接口信息,参数env可选项包括:sandbox,prod和stage(在tox.ini中配置)
del dsl['environments']
_api_descriptor_depend = dict() # 存储api.yml中接口信息,除了跳过(skip_test)和环境配置(environments)的信息
testcases = list() # 存储接口名称(api_name)和描述(description)
for key, api_dsl in dsl.items():
if 'depend_on' not in api_dsl:
continue
desc = api_dsl['description'] if 'description' in api_dsl else ''
testcases.append((key, desc, ),)
_api_descriptor_depend[key] = api_dsl
_all_testcases_depend = tuple(testcases)
7. 处理接口信息中的url、params和headers
1)设置环境变量:os.environ['ENV'] = 'sandbox' ,对应配置文件tox.ini中的环境配置 ENV = {env:ENV:sandbox}
2)获取环境变量:os.getenv('ENV')
3) url:除了登录接口中,其他接口的url均未包含endpoint(即https://aa.bb.com),所以需要拼接
4)params:先从配置文件中获取参数,再从测试数据文件中获取参数。update()方法可更新字典的键值对。比如登录接口的账号和密码,如果不想维护到配置文件中,可以维护在测试数据文件中
5)headers: 同params,除了登录接口,其他接口均需要token值,token值来源于登录接口
def resolve(name, dsl):
env = os.getenv('ENV', None)
endpoint = _env['endpoint'] # 运行环境的url
enable_only = _env['bootstrap']['enabled_only'] if 'enabled_only' in dsl else None
if enable_only and enable_only != env: # 如果api.yml中的环境与tox.ini中环境不一致,则返回
return None
url = '%s%s' % (_env['endpoint'], dsl['url']) # 拼接完整的接口url
category = dsl['category'] if 'category' in dsl else '' # 接口类型,在接口配置文件中维护该参数
path_comp = [_project_root, 'fixtures', env, ]
if category:
path_comp.append(category)
path_comp.append('%s.json' % name) # "fixtures/测试环境"下的json文件,命名规范是"api_name.json"
fixture_file = os.path.join(*path_comp) # 拼接测试数据的文件路径
api = {
'url': url,
'method': dsl['method'],
'params': dict(),
'headers': dict(),
}
if 'headers' in dsl:
api['headers'].update(dsl['headers'])
if 'params' in dsl:
api['params'].update(dsl['params'])
disable_auth = dsl['disable_auth'] if 'disable_auth' in dsl else False
if not disable_auth: # 处理非登录接口的headers
global _access_token
api['headers']['Authorization'] = 'Bearer %s' % _access_token
apis = []
if not os.path.exists(fixture_file):
apis.append(api)
else: # 处理存在测试数据的接口
with open(fixture_file, encoding='utf-8') as f:
fixtures = json.load(f)
if isinstance(fixtures, dict):
fixtures = [fixtures]
for fixture in fixtures:
# api_copy = api.copy()
api_copy = copy.deepcopy(api)
if 'headers' in fixture:
api_copy['headers'].update(fixture['headers'])
if 'params' in fixture:
api_copy['params'].update(fixture['params'])
apis.append(api_copy)
return apis
8. 获取token
1)上面有提到,_env存储的是当前运行环境下的登录接口信息
2)发送登录接口请求后,用全局变量 _access_token 存储token的值,供其他接口使用
def _bootstrap(env):
global _env
api_dsl = _env['bootstrap'] # 运行环境相对应的登录接口信息
api_dsl = resolve('bootstrap', api_dsl)
api_dsl = api_dsl[0]
resp = api.call(api_dsl['url'], api_dsl['method'], api_dsl['params'], api_dsl['headers'])
if resp.status_code == 200:
result = resp.json() # 相当于json.loads(resp.text)
if result['success'] == True:
global _access_token
_access_token = result['access_token']
return
raise Exception('Login failed')
9. conftest.py文件配置
1)该文件是Pytest框架中的文件,名字是固定的;作用是:给测试用例提供前置准备工作和后置清理工作
2)该项目的前置工作(setup()函数):加载接口配置信息(包括需要和不需要接口依赖的接口)、登录后获取token
import settings
def pytest_sessionstart(session):
settings.setup()
def setup():
global _all_testcases, _api_descriptor
if _all_testcases:
return
env = os.getenv('ENV', None)
if not env or env not in ('stage', 'sandbox', 'prod', ):
raise Exception('Environment is not configured. Sould be stage, sandbox, or prod.')
_load_api_dsl(env) # 加载接口配置信息(metadata/api.yml)
_load_api_dsl_depend(env)
_bootstrap(env) # 登录后,获取access_token
10. 测试用例-不需要接口依赖
1)使用pytest.mark.parametrize装饰器实现用例参数化,里面写两个参数
2)第一个参数是一个字符串,里面有两个参数,api_name(接口名称)和description(描述)
3)第二个参数是一个列表,里面的数据是元组类型。类似这样[('api_name1','description1'),('api_name2','description2'),...]
4)lookup()函数返回的是接口配置文件中不带依赖的api_name下的接口信息
# -- coding: utf-8 --
import logging
import pytest
import api
import settings
@pytest.mark.parametrize('api_name, description',[case for case in settings.list_testcases()])
def test_all(api_name, description):
api_dsl = settings.lookup(api_name)
requests = settings.resolve(api_name, api_dsl)
for req in requests:
resp = api.call(req['url'], req['method'], req['params'], req['headers'])
result = resp.json()
#logging.debug(resp.text)
assert resp.status_code == 200
if result['success'] == False:
logging.debug(resp.text)
assert result['success']
result = result['result']
if 'expect' in api_dsl:
expectations = api_dsl['expect']
_evaluate(result, expectations)
def lookup(api_name):
global _api_descriptor
return _api_descriptor[api_name]
11. 测试用例-需要接口依赖
# 购买售卖中的产品(依赖产品列表中是否存在产品,如果存在,则购买某产品)
@pytest.mark.dependency(depends=["get_demand_list"])
@pytest.mark.parametrize('api_name', ['financial_invest_demand'])
def test_financial_invest_demand(api_name):
api_dsl = settings.lookup_depend(api_name)
depend_on_name = api_dsl['depend_on']
# 取依赖的接口
depend_api_dsl = settings.lookup_depend(depend_on_name)
depend_requests = settings.resolve(depend_on_name, depend_api_dsl)
requests = depend_requests
req = requests[0]
# 取出product_id
resp_depend = api.call(req['url'], req['method'], req['params'], req['headers'])
result_depend = resp_depend.json()
product_id = result_depend["result"]['products'][0]["id"]
# 替换参数
requests = settings.resolve(api_name, api_dsl)
req = requests[0]
req['params'].update(
{
"product_id": product_id,
}
)
resp = api.call(req['url'], req['method'], req['params'], req['headers'])
result = resp.json()
assert resp.status_code == 200
if result['success'] == False:
logging.debug(resp.text)
assert result['success']
12. 执行测试用例
1)进入项目根目录后,执行命令:tox,相当于tox sandbox。即如果tox后面不加测试环境名称,则使用tox.ini中默认配置[testenv]中配置的环境sandbox;
默认执行的是pytest管理的全部的用例文件。tox + 测试用例文件相对路径,可指定执行某个用例文件
2)执行命令:tox -e sandbox或者tox -e prod,则可分别指定执行环境sandbox和prod下的测试用例。但是需要在配置文件tox.ini中添加如下配置信息
[testenv:sandbox]
setenv =
ENV = {env:ENV:sandbox}
PYTHONIOENCODING = utf-8
[testenv:prod]
setenv =
ENV = {env:ENV:prod}
PYTHONIOENCODING = utf-8
13. git地址
https://github.com/ChangYixue/api-test.git
三、Jenkins任务配置
1. Description:对构建任务的描述
2. Discard old builds:丢弃旧的构建,设置”保持构建的天数“和”保持构建的最大个数“
3. This project is parameterized:设置参数,添加字符串类型参数。参数添加成功后,构建时可以输入参数
4. Source Code Management:源码管理,配置github的url和凭证
5. Build Triggers:构建任务的触发器
在Authentication Token中指定TOKEN_NAME,然后可以通过连接JENKINS_URL/job/JOBNAME/build?token=TOKEN_NAME来启动build
6. Build-Execute shell:构建前执行shell命令
7. Post-build Actions:构建后操作,生成测试报告
8. E-mail Notification:构建后发送邮件到指定的邮箱
更多推荐
所有评论(0)