pytest+request+allure接口自动化框架搭建分享

介绍分享一个接口自动化框架搭建方法 (pytest+request+allure),这个方案是由 xpcs  同学在TesterHome社区网站的分享。

写在前面

去年11月被裁,到现在还没上岸,gap 半年了。上岸无望,专业技能不能落下,花了两三天时间,把之前工作中搭建使用的接口自动化框架,重写了一套。

楼主代码菜鸡,代码可能比较 low - -,希望通过本次分享,给社区里想写接口自动化的同学一些借鉴,也希望社区里的大神多给一些优化建议,大家互帮互助,共同进步~

  • 框架基于 python 语言,框架使用 pytest,报告使用 allure

  • 支持多环境运行,通过命令行传参区分

  • 支持多进程跑测,用例需独立无依赖,conftest.py 中包含多进程下只运行一次的 fileLock fixture

  • 支持数据库连接单例,一个库在一个进程下只会建立一次连接

  • 支持 mysql、redis 操作

  • 支持 get、post、put、delete 请求方法,请求是通过用例的请求头 Content-Type 来区分,是使用 params、data 还是 json 传参

  • 支持参数化数据驱动,用参数化参数字典,去更新通用参数字典,更新后发起请求

以下使用 windows 环境

conda 配置和新建工程:

安装conda
https://www.anaconda.com/download/success

新建工程,在 pycharm 中新建 conda 虚拟环境

图片

安装 allure 报告

https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/2.29.0/

图片

解压并配置环境变量
D:\allure\allure-2.29.0

图片

图片

cmd 命令行,验证 allure 安装

图片

设置 pycharm 文件编码 UTF-8

图片

依赖安装

pycharm 命令行运行
激活虚拟环境
conda activate pytest_api_auto
安装 python3.8
conda install python=3.8
安装依赖包
pip install requests
pip install jsonpath
pip install pytest
pip install allure-pytest
pip install pytest-sugar
pip install pytest-xdist
pip install pytest-assume
pip install pymysql
pip install redis
pip install faker
pip install filelock

目录划分

图片

以下是源码部分

封装log日志工具类

 
  1. # common/log_util.pyimport loggingimport os# create loggerlog = logging.getLogger("pytest_api_auto")log.setLevel(logging.INFO)# create file handler

  2. # mode 默认为a追加模式,如果修改为w为覆盖模式,多进程运行会出现日志缺失和错乱

  3. # 获取项目根目录拼接,日志会存在工程根目录pytest.log 每次运行追加写入fh = logging.FileHandler(os.path.join(os.path.dirname(os.path.dirname(__file__)), "pytest.log"),

  4. mode='a', encoding='UTF-8')fh.setLevel(logging.INFO)# create stream handlersh = logging.StreamHandler(stream=None)# create formatterfmt = "%(asctime)s - %(filename)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s"formatter = logging.Formatter(fmt)# add handler and formatter to loggerfh.setFormatter(formatter)sh.setFormatter(formatter)log.addHandler(fh)log.addHandler(sh)

封装环境配置

 
  1. config/env.pyfrom common.log_util import logclass ENV:

  2. # 环境信息:test 测试 prod 准生产 # 从pytest命令行获取 info = None# 测试环境服务域名配置class UrlTestConfig:

  3. api_backend = "http://api_backend.cn:8899"# 准生产环境服务域名配置class UrlProdConfig:

  4. api_backend = "http://api_backend.cn:8899"def get_url(server_name):

  5. if ENV.info == "test":

  6. url = getattr(UrlTestConfig, server_name)

  7. log.info(f"测试环境获取服务域名 - {server_name} : {url}")

  8. return url

  9. elif ENV.info == "prod":

  10. url = getattr(UrlProdConfig, server_name)

  11. log.info(f"准生产环境获取服务域名 - {server_name} : {url}")

  12. return url

  13. else:

  14. raise Exception("--env 环境信息有误")

封装 mysql 操作工具类

 
  1. # common/mysql_util.py

  2. # 装饰器,同一个mysql数据库只建立一次连接import pymysqlfrom time import sleepfrom common.log_util import log# 装饰器,同一个mysql数据库只建立一次连接def decorate_single(cls):

  3. connect_list = {}

  4. def wrapper(*args, **kwargs):

  5. nonlocal connect_list

  6. db_name = args[0]["db"]

  7. if db_name not in connect_list:

  8. connect_list[db_name] = cls(*args, **kwargs)

  9. log.info(f"建立mysql连接并返回 - {db_name}")

  10. else:

  11. log.info(f"mysql连接已建立,直接返回 - {db_name}")

  12. return connect_list[db_name]

  13. return wrapper@decorate_singleclass MySql:

  14. def __init__(self, db_config: dict):

  15. """

  16. :params: db_config 数据库配置 类型为字典

  17. """

  18. # 数据库配置 # autocommit: True 选项很关键,如果不设置,新增数据无法查出 # mysql默认数据引擎是innodb 默认数据隔离级别重复读,如果事务不提交,那么每次查询,查询都是同一块数据快照 self.conn = None

  19. while True:

  20. try:

  21. self.conn = pymysql.connect(**db_config)

  22. break

  23. # 数据库连接,偶尔会连接不上 # 报错 pymysql.err.OperationalError: (2013, 'Lost connection to MySQL server during query') # 解决办法,就是重新连接 except pymysql.err.OperationalError:

  24. log.warning("连接失败,可能环境不稳定,重新连接!")

  25. sleep(1)

  26. except Exception as e:

  27. log.warning("获取mysql连接失败!请检查数据库配置或网络连接")

  28. raise e

  29. def fetchone(self, sql_str: str):

  30. """

  31. :params: sql_str 数据库sql

  32. :return: 返回查询结果的一条记录,类型是字典; 若未查询到,则返回None

  33. """

  34. try:

  35. with self.conn.cursor(pymysql.cursors.DictCursor) as cursor:

  36. log.info(f"执行sql: {sql_str}")

  37. cursor.execute(sql_str)

  38. data = cursor.fetchone()

  39. log.info(f"sql执行结果: {data}")

  40. return data

  41. except Exception as e:

  42. log.warning("执行sql失败!")

  43. raise e

  44. def fetchall(self, sql_str: str):

  45. """

  46. :params: sql_str 数据库sql

  47. :return: 返回查询结果的全部记录,类型是列表,列表元素为字典

  48. """

  49. try:

  50. with self.conn.cursor(pymysql.cursors.DictCursor) as cursor:

  51. log.info(f"执行sql: {sql_str}")

  52. cursor.execute(sql_str)

  53. data = cursor.fetchall()

  54. log.info(f"sql执行结果: {data}")

  55. return data

  56. except Exception as e:

  57. log.warning("执行sql失败!")

  58. raise e

  59. def execute_dml(self, sql_str):

  60. """

  61. function: 执行insert、update、delete

  62. :param sql_str 数据库sql

  63. :return: 无返回

  64. """

  65. try:

  66. with self.conn.cursor(pymysql.cursors.DictCursor) as cursor:

  67. log.info(f"执行sql: {sql_str}")

  68. data = cursor.execute(sql_str)

  69. # 提交操作,我们配置连接是自动提交,所以下面提交步骤也可省略 self.conn.commit()

  70. log.info(f"sql执行结果: {data}")

  71. except Exception as e:

  72. log.warning("执行sql失败!")

  73. raise e

  74. def close(self):

  75. """

  76. function:关闭数据库连接

  77. params: conn 数据库连接

  78. """

  79. self.conn.close()

封装 mysql 配置和连接获取

 
  1. # config/mysql.pyfrom common.mysql_util import MySqlfrom config.env import ENVfrom common.log_util import log# 数据库连接配置class MysqlTestConfig:

  2. """Mysql测试环境配置"""

  3. api_auto = {'host': 'localhost', 'port': 3306,

  4. 'db': 'api_auto', 'user': 'root',

  5. 'password': 'root', 'autocommit': True

  6. }class MysqlProdConfig:

  7. """Mysql准生产环境配置"""

  8. api_auto = {'host': 'localhost', 'port': 3306,

  9. 'db': 'api_auto', 'user': 'root',

  10. 'password': 'root', 'autocommit': True

  11. }def get_mysql_conn(db_name):

  12. if ENV.info == "test":

  13. log.info("测试环境建立mysql连接 - " + db_name)

  14. return MySql(getattr(MysqlTestConfig, db_name))

  15. elif ENV.info == "prod":

  16. log.info("准生产环境建立mysql连接 - " + db_name)

  17. return MySql(getattr(MysqlProdConfig, db_name))

  18. else:

  19. raise Exception("--env 环境信息有误")

封装 redis 工具类

 
  1. # common/redis_util.pyimport redisfrom common.log_util import log# 装饰器,同一个redis只建立一次连接def decorate_single(cls):

  2. connect_list = {}

  3. def wrapper(*args, **kwargs):

  4. nonlocal connect_list

  5. host = args[0]["host"]

  6. if host not in connect_list:

  7. connect_list[host] = cls(*args, **kwargs)

  8. log.info(f"建立redis连接并返回 - {host}")

  9. else:

  10. log.info(f"redis连接已建立,直接返回 - {host}")

  11. return connect_list[host]

  12. return wrapper@decorate_singleclass Redis:

  13. def __init__(self, db_config):

  14. """

  15. :params: db_config 数据库配置 类型为字典

  16. """

  17. self.pool = redis.ConnectionPool(**db_config)

  18. self.rs = redis.Redis(connection_pool=self.pool)

  19. def del_key(self, key):

  20. """

  21. :param key: redis key str字符类型

  22. :return: 删除成功返回 True 否则 False

  23. """

  24. log.info(f"redis 删除key {key}")

  25. if self.rs.delete(key) == 1:

  26. log.info(f"key {key} 删除成功")

  27. return True

  28. else:

  29. log.warning(f"key: {key} 不存在!")

  30. return False

  31. def del_keys(self, keys_pattern):

  32. """

  33. :param keys_pattern: key通配符 str字符类型 ex: *name*

  34. :return:删除成功返回 True 否则 False

  35. """

  36. log.info(f"redis 删除keys 通配符 {keys_pattern}")

  37. keys = self.rs.keys(keys_pattern)

  38. if keys:

  39. log.info(f"redis 删除keys {keys}")

  40. for k in keys:

  41. self.rs.delete(k)

  42. log.info(f"keys {keys} 删除成功")

  43. return True

  44. else:

  45. log.warning("通配符未匹配到key!")

  46. return False

  47. def set(self, key, value, ex=8 * 60 * 60):

  48. """

  49. 操作str类型

  50. :param key: redis key str字符类型

  51. :param value: str字符类型

  52. :param ex: 数据超时时间,默认8小时

  53. return: 写入成功返回 True

  54. """

  55. log.info(f"redis str类型 数据写入 key: {key} value: {value}")

  56. return self.rs.set(key, value, ex=ex)

  57. def get(self, key):

  58. """

  59. 操作str类型

  60. :param key: redis key str字符类型

  61. :return: 获取到返回str字符类型 # 未获取到返回 None

  62. """

  63. data = self.rs.get(key)

  64. log.info(f"redis str类型 数据获取 key: {key} value: {data}")

  65. return data

  66. def lrange(self, key):

  67. """

  68. 操作list类型

  69. :param key: redis key str字符类型

  70. return: 获取到返回list列表类型 # 未获取到返回空列表 []

  71. """

  72. data = self.rs.lrange(key, 0, -1)

  73. log.info(f"redis list类型 数据获取 key: {key} values: {data}")

  74. return data

  75. def smembers(self, key):

  76. """

  77. 操作 set 集合

  78. :param key: redis key str字符类型

  79. return: 获取到返回set集合类型 # 未获取到返回空集合 set()

  80. """

  81. data = self.rs.smembers(key)

  82. log.info(f"redis set类型 数据获取 key: {key} values: {data}")

  83. return data

  84. def zrange(self, key):

  85. """

  86. 操作 zset 有序集合

  87. :param key: redis key str字符类型

  88. return: 获取到返回list列表类型 # 未获取到返回空列表 []

  89. """

  90. data = self.rs.zrange(key, 0, -1)

  91. log.info(f"redis zset类型 数据获取 key: {key} values: {data}")

  92. return data

  93. # hash 操作 hset hget 后续可扩展

  94. def close(self):

  95. """

  96. function:关闭数据库连接

  97. params: rs Redis对象

  98. """

  99. self.rs.close()

封装 redis 配置和连接获取

 
  1. # config/redis.pyfrom common.redis_util import Redisfrom config.env import ENVfrom common.log_util import logclass RedisTestConfig:

  2. api_backend = {'host': 'api_backend.cn', 'password': 'redis123',

  3. 'port': 6379, 'db': 0, 'decode_responses': True}class RedisProdConfig:

  4. api_backend = {'host': 'api_backend.cn', 'password': 'redis123',

  5. 'port': 6379, 'db': 0, 'decode_responses': True}def get_redis_conn(name):

  6. if ENV.info == "test":

  7. log.info("测试环境建立redis连接 - " + name)

  8. return Redis(getattr(RedisTestConfig, name))

  9. elif ENV.info == "prod":

  10. log.info("准生产环境建立redis连接 - " + name)

  11. return Redis(getattr(RedisProdConfig, name))

  12. else:

  13. raise Exception("--env 环境信息有误")

封装 requests 工具类

 
  1. # common/requests_util.pyimport requestsfrom common.log_util import logdef send_request(url, method, data, headers, **kwargs):

  2. """

  3. :param url: 请求域名 类型 str ex: http://xxx.com/path

  4. :param method: 请求方法 类型 str 暂时支持 get、post、put、delete

  5. :param data: 请求数据,类型 dict、list、str

  6. :param headers: 请求头,类型 dict

  7. :param kwargs: 扩展支持 files 上传文件、proxy 代理等

  8. :return:

  9. """

  10. if not url.startswith("http://") and not url.startswith("https://"):

  11. raise Exception("请求url缺少协议名")

  12. if method.lower() not in ("get", "post", "put", "delete"):

  13. raise Exception(f"暂不支持请求方法 - {method} - 可后续扩展")

  14. log.info("请求参数:")

  15. log.info(f"url: {url}")

  16. log.info(f"method: {method}")

  17. log.info(f"data: {data}")

  18. log.info(f"headers: {headers}")

  19. log.info(f"kwargs: {kwargs}")

  20. try:

  21. if "Content-Type" in headers.keys():

  22. # headers 包含传参类型 if headers["Content-Type"] in ("application/x-www-form-urlencoded", "multipart/form-data"):

  23. res = requests.request(url=url, method=method, data=data, headers=headers, timeout=30, **kwargs)

  24. elif headers["Content-Type"] == "application/json":

  25. res = requests.request(url=url, method=method, json=data, headers=headers, timeout=30, **kwargs)

  26. else: # 若非上面三种类型,默认使用json传参 text/html, text/plain等,可后续扩展验证 res = requests.request(url=url, method=method, json=data, headers=headers, timeout=30, **kwargs)

  27. else:

  28. # 请求头没指定传参类型Content-Type,则使用params传参,即在url中传参,如get请求 res = requests.request(url=url, method=method, params=data, headers=headers, timeout=30, **kwargs)

  29. except Exception as e:

  30. log.warning("请求发生异常!!!")

  31. raise e

  32. if res.status_code == 200:

  33. log.info("请求成功")

  34. log.info("响应参数:")

  35. log.info(f"{res.text}")

  36. else:

  37. log.warning(f"请求失败!!! 返回码不为200, 状态码为: {res.status_code}")

  38. log.warning(f"响应参数:")

  39. log.warning(f"text: {res.text}")

  40. log.warning(f"raw: {res.raw}")

  41. raise Exception("返回码不为200")

  42. try:

  43. # 返回为字典类型 return res.json()

  44. except requests.exceptions.JSONDecodeError:

  45. log.warning("响应参数不为json,返回响应 response对象")

  46. return res

封装用例数据解析

 
  1. # common/data_parser.pyfrom config.env import get_urlfrom common.request_util import send_requestimport jsonpathfrom common.log_util import logdef parser(server_name, data_dict, param=None, **kwargs):

  2. """

  3. :param server_name: env.py 中的服务域名 类型str ex: api_backend

  4. :param data_dict: test_xxx.py测试用例对应data.py中的接口请求数据字典 ex: api_backend["get_student"]

  5. :param param: data.py 参数化列表中一项中的data ex: api_backend["get_student"]["param_list"][0]["data"]

  6. :**kwargs: 扩展参数

  7. :return: 请求结果,如果响应是json类型返回dict,否则返回response对象

  8. """

  9. # 获取配置中的服务器域名,拼接path url = get_url(server_name) + data_dict["path"]

  10. method = data_dict["method"]

  11. headers = data_dict["headers"]

  12. data = data_dict["data"]

  13. # 参数化后发起请求,用参数化参数更新或替代通用参数 if param:

  14. if isinstance(data, dict) and isinstance(param, dict):

  15. # 如果通用参数为字典,参数化参数也为字典,使用参数化参数更新通用参数 ex: {"xx": "xx"} data.update(param)

  16. else:

  17. # 如果通用参数是字符串、列表(元素为字符、数字、字典),直接使用参数化参数据替换通用参数 ex: ["xx", "xx"] data = param

  18. res = send_request(url, method, data, headers, **kwargs)

  19. return resdef assert_res(res_dict, expect_dict):

  20. """

  21. :param res_dict: request请求返回的结果字典,类型 dict

  22. :param expect_dict: 预期结果字典, 类型 dict

  23. """

  24. if isinstance(res_dict, dict):

  25. log.info("开始断言")

  26. log.info(f"预期结果: {expect_dict}")

  27. # 遍历预期结果的key,使用jsonpath获取请求结果的value,与预期结果value比对 for k in expect_dict.keys():

  28. res_list = jsonpath.jsonpath(res_dict, '$..' + str(k)) # 返回列表 assert expect_dict[k] in res_list

  29. log.info("断言通过")

  30. else:

  31. log.warning("请求结果不为dict字典类型,跳过断言!")

封装 faker 模拟数据

 
  1. # common/faker.pyfrom faker import Fakerfrom common.log_util import logfake = Faker("zh_CN")def get_name():

  2. name = fake.name()

  3. log.info(f"faker 生成姓名: {name}")

  4. return namedef get_phone_number():

  5. phone_number = fake.phone_number()

  6. log.info(f"faker 生成手机号: {phone_number}")

  7. return phone_numberdef get_id_card():

  8. id_card = fake.ssn()

  9. log.info(f"faker 生成身份证号: {id_card}")

  10. return id_card

pytest.ini

 
  1. [pytest]addopts = -p no:warnings -vsmarkers =

  2. multiprocess: suppurt mutl-process execute cases

全局 conftest.py

 
  1. # conftest.pyimport pytestfrom common.log_util import logfrom filelock import FileLockimport jsonfrom config.env import ENVimport osimport allure# 自定义环境信息pytest命令行def pytest_addoption(parser):

  2. parser.addoption(

  3. "--env",

  4. action="store",

  5. default="test",

  6. help="set pytest running environment ex: --env=test --env=prod"

  7. )# 从pytest命令行获取环境信息@pytest.fixture(scope="session")def get_env(request):

  8. ENV.info = request.config.getoption("--env")

  9. log.info("运行环境: " + ENV.info)

  10. return ENV.info# 终结函数,最后执行@pytest.fixture(scope="session", autouse=True)def fixture_case_end(request):

  11. def case_end():

  12. log.info("测试结束")

  13. request.addfinalizer(case_end)@pytest.fixture(scope="session", autouse=True)# fixture 嵌套先执行获取环境信息get_env

  14. # 加入 tmp_path_factory worker_id 用于多进程执行 # 多进程运行,token只获取一次def fixture_get_token(get_env, tmp_path_factory, worker_id):

  15. # 单进程执行 if worker_id == "master":

  16. # 获取token token = {"token": "xpcs"}

  17. log.info("fixture_get_token master获取token %s" % token['token'])

  18. else:

  19. # 多进程执行 root_tmp_dir = tmp_path_factory.getbasetemp().parent

  20. fn = root_tmp_dir / "data.json"

  21. # 这里with里面的语句,理解为是被加锁的,同一时间只能有一个进程访问 with FileLock(str(fn) + ".lock"):

  22. if fn.is_file():

  23. # session_fixture 获取token已执行,直接从文件中读取token token = json.loads(fn.read_text())

  24. log.info("fixture_get_token slave使用token %s" % token['token'])

  25. else:

  26. token = {"token": "xpcs"}

  27. fn.write_text(json.dumps(token))

  28. log.info("fixture_get_token slave获取token %s" % token['token'])

  29. yield token['token']

  30. # session 结束后自动执行如下 log.info("session结束")# 用例失败自动执行钩子函数@pytest.hookimpl(tryfirst=True, hookwrapper=True)def pytest_runtest_makereport(item):

  31. # 获取钩子方法的调用结果 outcome = yield

  32. rep = outcome.get_result()

  33. # 仅仅获取用例call 执行结果是失败的情况, 不包含 setup/teardown if rep.when == "call" and rep.failed:

  34. mode = "a" if os.path.exists("failures") else "w"

  35. with open("failures", mode) as f:

  36. # let's also access a fixture for the fun of it if "tmpdir" in item.fixturenames:

  37. extra = " (%s)" % item.funcargs["tmpdir"]

  38. else:

  39. extra = ""

  40. f.write(rep.nodeid + extra + "\n")

  41. with allure.step("用例运行失败,可加入信息"):

  42. allure.attach("失败内容: ----xpcs----", "失败标题", allure.attachment_type.TEXT)

测试用例

 
  1. # line_of_business/service_name_api_backend/test_api_backend.pyimport pytestfrom time import sleepimport allurefrom common.data_parser import parser, assert_resfrom config.mysql import get_mysql_connfrom common.log_util import logfrom line_of_business_name.service_name_api_backend.data import api_backendfrom common.faker_util import get_name, get_id_card, get_phone_numberfrom config.redis import get_redis_conn@allure.feature("flask后端接口测试")class TestApiBackend:

  2. @classmethod

  3. def setup_class(cls):

  4. # 获取数据库连接,执行sql测试 log.info("setup_class")

  5. # 数据库连接根据db库名单例,相同库返回同一个连接 conn = get_mysql_conn("api_auto")

  6. conn1 = get_mysql_conn("api_auto")

  7. conn.execute_dml("insert into test_xdist(msg) values ('%s')" % "class_setup-数据库写入测试")

  8. conn1.fetchone("select * from test_xdist limit 1")

  9. @classmethod

  10. def teardown_class(cls):

  11. log.info("steup_teardowm")

  12. # 获取redis连接,执行命令测试 # redis连接根据host单例,相同host返回同一个连接 rs = get_redis_conn("api_backend")

  13. rs1 = get_redis_conn("api_backend")

  14. rs.set("name", "xp")

  15. rs1.get("name")

  16. @allure.story("测试故事1")

  17. @pytest.mark.xfail(reason='预期失败用例')

  18. @user12ize("param", [{"title": "标题1", "param": 2, "assert": 3}])

  19. def test_case_one(self, param):

  20. sleep(1)

  21. allure.dynamic.description("测试故事1-描述信息")

  22. allure.dynamic.severity(allure.severity_level.CRITICAL) # 用例级别严重 # allure动态标题 allure.dynamic.title(param["title"])

  23. log.info("测试faker数据")

  24. log.info(f"{get_name()} {get_phone_number()} {get_id_card()}")

  25. # pytest.assume(False) # 多重断言插件,断言失败继续执行下面 assert param["param"] + 2 == param["assert"]

  26. @allure.story("查询学生接口")

  27. @user14cess # 此用例分组到可多进程跑测 @user15ize("param", api_backend["get_student"]["param_list"])

  28. def test_get_student(self, param, fixture_get_token):

  29. sleep(1)

  30. allure.dynamic.title(param["title"])

  31. data_dict = api_backend["get_student"]

  32. data_dict["headers"]["Cookie"] = fixture_get_token

  33. res = parser("api_backend", data_dict, param["data"])

  34. assert_res(res, param["assert"])

  35. @allure.story("新增学生接口")

  36. @user17cess # 此用例分组到可多进程跑测 @user18ize("param", api_backend["post_student"]["param_list"])

  37. def test_post_student(self, param):

  38. sleep(1)

  39. allure.dynamic.title(param["title"])

  40. data_dict = api_backend["post_student"]

  41. res = parser("api_backend", data_dict, param["data"])

  42. assert_res(res, param["assert"])

  43. @allure.story("更新学生接口")

  44. @user20cess # 此用例分组到可多进程跑测 @user21ize("param", api_backend["put_student"]["param_list"])

  45. def test_put_student(self, param):

  46. sleep(1)

  47. allure.dynamic.title(param["title"])

  48. data_dict = api_backend["put_student"]

  49. res = parser("api_backend", data_dict, param["data"])

  50. assert_res(res, param["assert"])

用例数据驱动

 
  1. # line_of_business/service_name_api_backend/data.py

  2. # 服务名外层大字典,参数key是接口名,value是接口的请求信息字典,用例模块可通过接口名引用接口信息字典

  3. # param_list 参数化列表,用于pytest参数化,每次选取其中一项的data,去更新外部data通用参数,发起请求api_backend = {

  4. "get_student": dict(path="/student",

  5. method="get",

  6. # headers 不包含Content-Type 则request使用params传参 headers={},

  7. # 通用参数,每次请求使用 data={"test": "test"},

  8. # 参数化参数,每次使用其中一项,更新通用参数 param_list=[

  9. {"title": "获取学生信息-张三", "data": {"name": "张三"}, "assert": {"code": 0, "msg": "ok"}},

  10. {"title": "获取学生信息-李四", "data": {"name": "李四"}, "assert": {"code": 0, "msg": "ok"}},

  11. {"title": "获取学生信息-王五", "data": {"name": "王五"}, "assert": {"code": 0, "msg": "ok"}}

  12. ]),

  13. 'post_student': dict(path="/student",

  14. method="post",

  15. # headers Content-Type = application/x-www-form-urlencoded 则使用 request使用data传参 headers={"Cookie": "", "Content-Type": "application/x-www-form-urlencoded"},

  16. data={"test": "test"},

  17. param_list=[

  18. {"title": "新增学生信息-张三", "data": {"name": "张三"}, "assert": {"code": 1, "msg": "ok"}},

  19. {"title": "新增学生信息-李四", "data": {"name": "李四"}, "assert": {"code": 0, "msg": "ok"}},

  20. {"title": "新增学生信息-王五", "data": {"name": "王五"}, "assert": {"code": 0, "msg": "ok"}}

  21. ]),

  22. 'put_student': dict(path="/student",

  23. method="put",

  24. # headers Content-Type = application/json 则使用 request使用json传参 headers={"Cookie": "", "Content-Type": "application/json"},

  25. data={"test": "test"},

  26. param_list=[

  27. {"title": "更新学生信息-张三", "data": {"name": "张三"}, "assert": {"code": 0, "msg": "ok"}},

  28. {"title": "更新学生信息-李四", "data": {"name": "李四"}, "assert": {"code": 0, "msg": "okk"}},

  29. {"title": "更新学生信息-王五", "data": {"name": "王五"}, "assert": {"code": 0, "msg": "ok"}}

  30. ])}

调试运行入口

 
  1. # run.pyimport pytestimport os# 用例调试入口if __name__ == '__main__':

  2. pytest.main([r"line_of_business_name", "--clean-alluredir", "--alluredir=allure_result", "--cache-clear", "--env=prod"])

  3. # pytest.main([r"-m multiprocess", "--clean-alluredir", "--alluredir=allure_result", "-n 3", "--cache-clear", "--env=prod"]) os.system(r"allure generate allure_result -c -o allure_report")

  4. os.system(r"allure open -h 127.0.0.1 -p 8899 allure_report")

失败用例重跑

 
  1. # failed_run.pyimport pytestimport os# 失败用例重跑if __name__ == '__main__':

  2. pytest.main([r"line_of_business_name", "--lf", "--clean-alluredir", "--alluredir=allure_result", "--env=prod"])

  3. os.system(r"allure generate allure_result -c -o allure_report")

  4. os.system(r"allure open -h 127.0.0.1 -p 8899 allure_report")

报告展示

 

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取   

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/912124.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

大数据新视界 -- 大数据大厂之 Impala 性能优化:融合机器学习的未来之路(上 (2-2))(11/30)

💖💖💖亲爱的朋友们,热烈欢迎你们来到 青云交的博客!能与你们在此邂逅,我满心欢喜,深感无比荣幸。在这个瞬息万变的时代,我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Unity 实现数字垂直滚动效果

Unity 实现数字垂直滚动效果 前言项目场景布置Shader代码编写材质球设置代码编写数字图片 前言 遇到一个需要数字垂直滚动模拟老虎机的效果,记录一下。 项目 场景布置 3个Image换上带有RollNumberShader的材质 在RollNumberScript脚本中引用即可 Shader代码编…

[linux]docker基础

常见命令 Docker最常见的命令就是操作镜像、容器的命令,详见官方文档: Docker Docs 案例: 查看DockerHub,拉取Nginx镜像,创建并运行Nginx容器 在DockerHub中搜索Nginx镜像 拉取Nginx镜像 查看本地镜像列表 把镜像保持到本地 查看保持命令的…

纯C++信号槽使用Demo (sigslot 库使用)

sigslot 库与QT的信号槽一样,通过发送信号,触发槽函数,信号槽不是QT的专利,早在2002年国外的一小哥用C写了sigslot 库,简单易用; 该库的官网(喜欢阅读的小伙伴可以仔细研究)&#xf…

(Go语言)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法

0. 序言 从这章开始,在Go基础语法里难度就开始上来了 在学习函数与方法前,先弄明白指针是很重要的。 1. 指针 在没学指针前,相信很多人就已经大概知道指针是个什么东西了。因为它太有名了,当然是与 C和C 的出名有关。 1.1 指针…

基于redis实现API接口访问次数限制

一,概述 日常开发中会有一个常见的需求,需要限制接口在单位时间内的访问次数,比如说某个免费的接口限制单个IP一分钟内只能访问5次。该怎么实现呢,通常大家都会想到用redis,确实通过redis可以实现这个功能&#xff0c…

实在智能受邀出席柳州市智能终端及机器人产业发展合作大会

10 月 27 日至 28 日,由中共柳州市委员会与柳州市人民政府主办的2024柳州市智能终端及机器人产业发展合作大会在柳州莲花山庄隆重举行。大会充分整合各方资源,持续深化与柳州在重大战略规划、重大平台建设、重点产业培育等领域的合作。作为智能体行业的知…

JDBC-PreparedStatement

在前面使用的Statement中,编写sql语句使用的是拼接的形式,这样不仅可读性差,还非常容易导致出错,最大的问题是安全问题。 sql注入 在需要用户输入的地方,用户输入的是SQL语句的片段,最终用户输入的SQL片段…

如何创建备份设备以简化 SQL Server 备份过程?

SQL Server 中的备份设备是什么? 在 SQL Server 中,备份设备是用于存储备份数据的物理或逻辑介质。备份设备可以是文件、设备或其他存储介质。主要类型包括: 文件备份设备:通常是本地文件系统中的一个或多个文件。可以是 .bak 文…

非计算机背景但是想从事医学AI研究,需要掌握的编程语言|个人观点·24-11-08

小罗碎碎念 目前,我们从事医学AI研究的,接触的最多的两种编程语言应该就是R和Python了。那么初学者很容易提出一个疑问,**我想从事医学AI相关的研究的话,应该学哪些编程语言呢?**在文章的开头,我可以先给出…

Jmeter基础篇(21)教你手动修改Jmeter测试报告和压测结果

哈喽呀各位小伙伴!今天给大家带来一期关于Jmeter黑科技的教学! 在日常性能测试过程中,我们经常使用JMeter这个强大的工具来执行压力测试,并通过JMeter的报告生成命令,从CSV或JTL文件中读取数据,生成HTML格式的测试报告。然而,测试报告生成之后,数据就是固定的了,很多…

AHB Matrix 四星级 验证笔记(2.4) Tt3.3AHB总线协议测试时的 并行数据

文章目录 前言一、代码二、错误1.地址范围2. 并行执行线程中变量覆盖的情况3.有关incr的beat 前言 来源路科验证本节搞定 T3.3 AHB总线协议的覆盖:AHB_PROTOCOL_COVER 即测试ahb slave接口和master接口支持(尽可能)全部的ahb协议传输场景&am…

IDA*算法 Power Calculus————poj 3134

目录 闲聊 前言 DFS算法的无效搜索 BFS算法的空间浪费 IDDFS A*算法 IDA* Power Calculus 问题描述 输入 输出 问题分析 代码 闲聊 前几周在忙着数学竞赛,所以就没时间更新,高等数学,一生之敌,真不知道报名的时候我是怎么想…

权限管理简单练习

1.修改/tmp的权限改为 rwxrwxrwx 2.添加SUID权限到/tmp 3.添加SBIT权限到/tmp目录 4. 使用rhel创建 /tmp/123.txt 5.使用其他非root账号删除 /tmp/123/txt 能否执行成功 6.普通用户除了使用sudo可以执行poweroff以外,还有什么别的办法可以执行poweroff

uni-app 图标库整合最佳实践:使用 iconfont 构建属于自己的图标库

一. 前言 在前端开发中,图标已经成为页面设计中不可或缺的一部分。图标可以使界面更加美观、清晰,并且能够提升用户体验。而使用图标库来管理和引用图标资源,可以带来更多的便利和效率。 而在众多的图标库中,iconfont 独树一帜。…

课程讲解--深入探究二分算法

一、二分查找算法的基本概念 定义与原理 二分查找,也被称为折半查找,是一种在有序数据集合中查找特定元素的高效算法。其原理基于分治思想,每次查找都将查找区间缩小一半。例如,在一个有序数组中查找一个特定的数字,我…

达梦数据库DM Exception字符串截断错误,略坑~

前言 我之前在使用达梦数据库的时候,遇到了很多很多的问题,主要对达梦数据库也不是很熟悉,它的语法和我所熟悉的mysql和postgresql有很大的区别。 今天,讲一下我之前遇到的一个问题。这个问题的起因是用达梦数据库迁移工具&…

Java版工程行业管理系统-提升工程项目的综合管理能力

工程项目管理涉及众多环节和角色,如何实现高效协同和信息共享是关键。本文将介绍一个采用先进技术框架的Java版工程项目管理系统,该系统支持前后端分离,功能全面,可满足不同角色的需求。从项目进度图表到施工地图,再到…

高考:心态、时间、知识,多维度攻略让你脱颖而出

高考,宛如一场无声的激战,承载着无数莘莘学子的梦想与热望。在这激烈的竞争中,充分且周全的准备显得尤为关键。那么,高考备考究竟应从哪些方面入手?又有哪些行之有效的备考策略能为我们保驾护航呢? 一、高考…

信息安全工程师(82)操作系统安全概述

一、操作系统安全的概念 操作系统安全是指操作系统在基本功能的基础上增加了安全机制与措施,从而满足安全策略要求,具有相应的安全功能,并符合特定的安全标准。在一定约束条件下,操作系统安全能够抵御常见的网络安全威胁&#xff…