目录

前言:

1. 该框架重构自之前的框架

2. 目录截图

一、tox简介和使用介绍

1. tox简介

2. 配置文件tox.ini

二、框架搭建

1. 封装接口请求

2. 配置接口信息

3. 维护测试数据

4. 配置日志

5. 加载接口配置信息

6. 加载有接口依赖的接口配置信息

7. 处理接口信息中的url、params和headers

8. 获取token

9. conftest.py文件配置

10. 测试用例-不需要接口依赖

11. 测试用例-需要接口依赖 

12. 执行测试用例

13. git地址

三、Jenkins任务配置


前言:

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:构建后发送邮件到指定的邮箱

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐