pytest 参数化进阶

目录

前言:

语法

参数化误区

实践

简要回顾


前言:

pytest是一个功能强大的Python测试框架,它提供了参数化功能,可以帮助简化测试用例的编写和管理。

语法

本文就赶紧聊一聊 pytest 的参数化是怎么玩的。

@pytest.mark.parametrize

@user1ize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):assert eval(test_input) == expected
  • 可以自定义变量,test_input 对应的值是"3+5" "2+4" "6*9",expected 对应的值是 8 6 42,多个变量用 tuple,多个 tuple 用 list

  • 参数化的变量是引用而非复制,意味着如果值是 list 或 dict,改变值会影响后续的 test

  • 重叠产生笛卡尔积

import pytest@user2ize("x", [0, 1])
@user3ize("y", [2, 3])
def test_foo(x, y):pass

@pytest.fixture()

@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
  • 只能使用 request.param 来引用

  • 参数化生成的 test 带有 ID,可以使用-k来筛选执行。默认是根据函数名[参数名]来的,可以使用 ids 来定义

// list
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
// function
@pytest.fixture(params=[0, 1], ids=idfn)

使用--collect-only命令行参数可以看到生成的 IDs。

参数添加 marker

我们知道了参数化后会生成多个 tests,如果有些 test 需要 marker,可以用 pytest.param 来添加

marker 方式

# content of test_expectation.py
import pytest@user7ize("test_input,expected",[("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)],
)
def test_eval(test_input, expected):assert eval(test_input) == expected

fixture 方式

# content of test_fixture_marks.py
import pytest@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
def data_set(request):return request.param
def test_data(data_set):pass

pytest_generate_tests

用来自定义参数化方案。使用到了 hook,hook 的知识我会写在《pytest hook》中,欢迎关注公众号 dongfanger 获取最新文章。

# content of conf.pydef pytest_generate_tests(metafunc):if "test_input" in metafunc.fixturenames:metafunc.parametrize("test_input", [0, 1])
# content of test.pydef test(test_input):assert test_input == 0
  • 定义在 conftest.py 文件中
  • metafunc 有 5 个属性,fixturenames,module,config,function,cls
  • metafunc.parametrize() 用来实现参数化
  • 多个 metafunc.parametrize() 的参数名不能重复,否则会报错

参数化误区

在讲示例之前,先简单分享我的菜鸡行为。假设我们现在需要对 50 个接口测试,验证某一角色的用户访问这些接口会返回 403。我的做法是,把接口请求全部参数化了,test 函数里面只有断言,伪代码大致如下

def api():params = []def func():return request()params.append(func)...@user9ize('req', api())
def test():res = req()assert res.status_code == 403

这样参数化以后,会产生50 个 tests,如果断言失败了,会单独标记为 failed,不影响其他 test 结果。咋一看还行,但是有个问题,在回归的时候,可能只需要验证其中部分接口,就没有办法灵活的调整,必须全部跑一遍才行。这是一个相对错误的示范,至于正确的应该怎么写,相信每个人心中都有一个答案,能解决问题就是 ok 的。我想表达的是,参数化要适当,不要滥用,最好只对测试数据做参数化

实践

本文的重点来了,参数化的语法比较简单,实际应用是关键。这部分通过 11 个例子,来实践一下。示例覆盖的知识点有点多,建议留大段时间细看。

1.使用 hook 添加命令行参数--all,"param1"是参数名,带--all 参数时是 range(5) == [0, 1, 2, 3, 4],生成 5 个 tests。不带参数时是 range(2)。

# content of test_compute.pydef test_compute(param1):assert param1 < 4
# content of conftest.pydef pytest_addoption(parser):parser.addoption("--all", action="store_true", help="run all combinations")
def pytest_generate_tests(metafunc):if "param1" in metafunc.fixturenames:if metafunc.config.getoption("all"):end = 5else:end = 2metafunc.parametrize("param1", range(end))

2.testdata 是测试数据,包括 2 组。test_timedistance_v0 不带 ids。test_timedistance_v1 带 list 格式的 ids。test_timedistance_v2 的 ids 为函数。test_timedistance_v3 使用 pytest.param 同时定义测试数据和 id。

# content of test_time.py
from datetime import datetime, timedeltaimport pytesttestdata = [(datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)),(datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),
]@user10ize("a,b,expected", testdata)
def test_timedistance_v0(a, b, expected):diff = a - bassert diff == expected@user11ize("a,b,expected", testdata, ids=["forward", "backward"])
def test_timedistance_v1(a, b, expected):diff = a - bassert diff == expecteddef idfn(val):if isinstance(val, (datetime,)):# note this wouldn't show any hours/minutes/secondsreturn val.strftime("%Y%m%d")@user12ize("a,b,expected", testdata, ids=idfn)
def test_timedistance_v2(a, b, expected):diff = a - bassert diff == expected@user13ize("a,b,expected",[pytest.param(datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1), id="forward"),pytest.param(datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1), id="backward"),],
)
def test_timedistance_v3(a, b, expected):diff = a - bassert diff == expected

3.兼容 unittest 的 testscenarios

# content of test_scenarios.py
def pytest_generate_tests(metafunc):idlist = []argvalues = []for scenario in metafunc.cls.scenarios:idlist.append(scenario[0])items = scenario[1].items()argnames = [x[0] for x in items]argvalues.append([x[1] for x in items])metafunc.parametrize(argnames, argvalues, ids=idlist, scope="class")scenario1 = ("basic", {"attribute": "value"})
scenario2 = ("advanced", {"attribute": "value2"})class TestSampleWithScenarios:scenarios = [scenario1, scenario2]def test_demo1(self, attribute):assert isinstance(attribute, str)def test_demo2(self, attribute):assert isinstance(attribute, str)

4.初始化数据库连接

# content of test_backends.py
import pytestdef test_db_initialized(db):# a dummy testif db.__class__.__name__ == "DB2":pytest.fail("deliberately failing for demo purposes")
# content of conftest.py
import pytestdef pytest_generate_tests(metafunc):if "db" in metafunc.fixturenames:metafunc.parametrize("db", ["d1", "d2"], indirect=True)class DB1:"one database object"class DB2:"alternative database object"@pytest.fixture
def db(request):if request.param == "d1":return DB1()elif request.param == "d2":return DB2()else:raise ValueError("invalid internal test config")

5.如果不加 indirect=True,会生成 2 个 test,fixt 的值分别是"a"和"b"。如果加了 indirect=True,会先执行 fixture,fixt 的值分别是"aaa"和"bbb"。indirect=True 结合 fixture 可以在生成 test 前,对参数变量额外处理。

import pytest@pytest.fixture
def fixt(request):return request.param * 3@user16ize("fixt", ["a", "b"], indirect=True)
def test_indirect(fixt):assert len(fixt) == 3

6.多个参数时,indirect 赋值 list 可以指定某些变量应用 fixture,没有指定的保持原值。

# content of test_indirect_list.py
import pytest@pytest.fixture(scope="function")
def x(request):return request.param * 3@pytest.fixture(scope="function")
def y(request):return request.param * 2@user19ize("x, y", [("a", "b")], indirect=["x"])
def test_indirect(x, y):assert x == "aaa"assert y == "b"

7.兼容 unittest 参数化

# content of ./test_parametrize.py
import pytestdef pytest_generate_tests(metafunc):# called once per each test functionfuncarglist = metafunc.cls.params[metafunc.function.__name__]argnames = sorted(funcarglist[0])metafunc.parametrize(argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist])class TestClass:# a map specifying multiple argument sets for a test methodparams = {"test_equals": [dict(a=1, b=2), dict(a=3, b=3)],"test_zerodivision": [dict(a=1, b=0)],}def test_equals(self, a, b):assert a == bdef test_zerodivision(self, a, b):with pytest.raises(ZeroDivisionError):a / b

8.在不同 python 解释器之间测试对象序列化。python1 把对象 pickle-dump 到文件。python2 从文件中 pickle-load 对象。

"""
module containing a parametrized tests testing cross-python
serialization via the pickle module.
"""
import shutil
import subprocess
import textwrapimport pytestpythonlist = ["python3.5", "python3.6", "python3.7"]@pytest.fixture(params=pythonlist)
def python1(request, tmpdir):picklefile = tmpdir.join("data.pickle")return Python(request.param, picklefile)@pytest.fixture(params=pythonlist)
def python2(request, python1):return Python(request.param, python1.picklefile)class Python:def __init__(self, version, picklefile):self.pythonpath = shutil.which(version)if not self.pythonpath:pytest.skip("{!r} not found".format(version))self.picklefile = picklefiledef dumps(self, obj):dumpfile = self.picklefile.dirpath("dump.py")dumpfile.write(textwrap.dedent(r"""import picklef = open({!r}, 'wb')s = pickle.dump({!r}, f, protocol=2)f.close()""".format(str(self.picklefile), obj)))subprocess.check_call((self.pythonpath, str(dumpfile)))def load_and_is_true(self, expression):loadfile = self.picklefile.dirpath("load.py")loadfile.write(textwrap.dedent(r"""import picklef = open({!r}, 'rb')obj = pickle.load(f)f.close()res = eval({!r})if not res:raise SystemExit(1)""".format(str(self.picklefile), expression)))print(loadfile)subprocess.check_call((self.pythonpath, str(loadfile)))@user22ize("obj", [42, {}, {1: 3}])
def test_basic_objects(python1, python2, obj):python1.dumps(obj)python2.load_and_is_true("obj == {}".format(obj))

9.假设有个 API,basemod 是原始版本,optmod 是优化版本,验证二者结果一致。

# content of conftest.py
import pytest@pytest.fixture(scope="session")
def basemod(request):return pytest.importorskip("base")@pytest.fixture(scope="session", params=["opt1", "opt2"])
def optmod(request):return pytest.importorskip(request.param)
# content of base.pydef func1():return 1
# content of opt1.pydef func1():return 1.0001
# content of test_module.py
def test_func1(basemod, optmod):assert round(basemod.func1(), 3) == round(optmod.func1(), 3)

10.使用 pytest.param 添加 marker 和 id。

# content of test_pytest_param_example.py
import pytest@user25ize("test_input,expected",[("3+5", 8),pytest.param("1+7", 8, marks=pytest.mark.basic),pytest.param("2+4", 6, marks=pytest.mark.basic, id="basic_2+4"),pytest.param("6*9", 54, marks=[pytest.mark.basic, pytest.mark.xfail], id="basic_6*9"),],
)
def test_eval(test_input, expected):assert eval(test_input) == expected

11.使用 pytest.raises 让部分 test 抛出 Error。

from contextlib import contextmanagerimport pytest// 3.7+ from contextlib import nullcontext as does_not_raise
@contextmanager
def does_not_raise():yield@user27ize("example_input,expectation",[(3, does_not_raise()),(2, does_not_raise()),(1, does_not_raise()),(0, pytest.raises(ZeroDivisionError)),],
)
def test_division(example_input, expectation):"""Test how much I know division."""with expectation:assert (6 / example_input) is not None

简要回顾

本文先讲了参数化的语法,包括 marker,fixture,hook 方式,以及如何给参数添加 marker,然后重点列举了几个实战示例。参数化用好了能节省编码,达到事半功倍的效果。

  作为一位过来人也是希望大家少走一些弯路

在这里我给大家分享一些自动化测试前进之路的必须品,希望能对你带来帮助。

(WEB自动化测试、app自动化测试、接口自动化测试、持续集成、自动化测试开发、大厂面试真题、简历模板等等)

相信能使你更好的进步!

点击下方小卡片

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

openGauss学习笔记-13 openGauss 简单数据管理-DELETE语句

文章目录 openGauss学习笔记-13 openGauss 简单数据管理-DELETE语句13.1 语法格式13.2 参数说明13.3 示例 openGauss学习笔记-13 openGauss 简单数据管理-DELETE语句 DELETE语句可以从指定的表里删除满足WHERE子句的行。如果WHERE子句不存在&#xff0c;将删除表中所有行&…

css 禁止多次点击导致的选中了目标div的文字

像下面这样的情况&#xff0c;就可以用这种方法避免掉 禁止多次点击&#xff0c;导致的&#xff0c;选中了目标div的文字 或者 禁止多次点击&#xff0c;导致&#xff0c;html结构被选中显示出来 .targetDiv {-webkit-user-select: none;-moz-user-select: none;-ms-user-sel…

【云原生】Docker的初步认识,安装与基本操作

一、Docker的相关知识 Docker是一个开源的应用容器引擎&#xff0c;基于go语言开发并遵循了apache2.0协议开源。 Docker是在Linux容器里运行应用的开源工具&#xff0c;是一种轻量级的“虚拟机”。 Docker 的容器技术可以在一台主机上轻松为任何应用创建一个轻量级的、可移植的…

SpringCloud整合Sentinel

文章目录 1、Sentinel介绍2、安装Sentinel控制台3、微服务整合Sentinel 1、Sentinel介绍 阿里开源的流量控制组件官网&#xff1a;https://sentinelguard.io/zh-cn/index.html承接了阿里双十一大促流量的核心场景&#xff0c;如秒杀、消息削峰填谷、集群流量控制、实时熔断下游…

Python自动化之pytest常用插件

目录 1、失败重跑 pytest-rerunfailures 2、多重校验 pytest-assume 3、设定执行顺序 pytest-ordering 4、用例依赖&#xff08;pytest-dependency&#xff09; 5.分布式测试(pytest-xdist) 6.生成报告&#xff08;pytest-html&#xff09; 1、失败重跑 pytest-rerunfailu…

Flutter 小技巧之滑动控件即将“抛弃” shrinkWrap 属性

相信对于 Flutter 开发的大家来说&#xff0c; ListView 的 shrinkWrap 配置都不会陌生&#xff0c;如下图所示&#xff0c;每当遇到类似的 unbounded error 的时候&#xff0c;总会有第一反应就是给 ListView 加上 shrinkWrap: true 就可以解决问题&#xff0c;那为什么现在会…

椒图——靶场模拟

先查看ip&#xff0c;10.12.13.232模拟的外网ip&#xff0c;其他的模拟内网ip&#xff0c;服务里面搭建好的漏洞环境。 #第一个测试项目&#xff0c;web风险发现 新建&#xff0c;下发任务&#xff0c;点威胁检测&#xff0c;webshell&#xff0c;点扫描任务&#xff0c;点新…

QT中QTimer的循环时间与槽函数执行时间以及在事件循环中触发,不同时间的结果分析

目录 当循环时间小于槽函数时间时&#xff1a; 当循环间隔时间大于槽函数时间时&#xff1a; 当存在两个定时器器&#xff0c;其中一个还是间隔100ms&#xff0c;另一个间隔1000ms&#xff1a; 当两个定时器的循环周期大于槽函数执行时间时 当在主程序中添加一个for循环…

js - 对forEach()函数的一些理解

1&#xff0c;定义和用法 定义&#xff1a; forEach() 方法用于调用数组的每个元素&#xff0c;并将元素传递给回调函数。注意: forEach() 对于空数组是不会执行回调函数的。 用法&#xff1a; // 箭头函数 forEach((element) > { /* … */ }) forEach((element, index) &…

mcu 启动流程

MCU启动流程 MCU启动流程 MCU启动流程1 MCU的启动方式2 MCU程序启动执行过程3 启动过程的执行工作4 keil调式过程验证5 调试文件map 1 MCU的启动方式 单片机的启动方式&#xff0c;以stm32为例&#xff0c;如下&#xff1a; 不同的下载方式对应的不同的启动方式&#xff0c;st…

truffle 进行智能合约测试

本方法使用了可视化软件Ganache 前两步与不使用可视化工具的步骤是一样的&#xff08;有道云笔记&#xff09;&#xff0c;到第三步的时候需要注意&#xff1a; 在truffle插件下找到networks目录&#xff0c;提前打开Ganache软件 在Ganache中选择连接或者新建&#xff0c;我在…

如何学习Java集合框架? - 易智编译EaseEditing

要学习Java集合框架相关的技术和知识&#xff0c;可以按照以下步骤进行&#xff1a; 掌握Java基础知识&#xff1a; 在学习集合框架之前&#xff0c;确保你已经具备良好的Java编程基础&#xff0c;包括语法、面向对象编程&#xff08;OOP&#xff09;原理和常用的核心类库等。…

MySQL备份与还原/索引/视图

MySQL备份与还原/索引/视图练习 文章目录 一、备份与还原1、使用mysqldump命令备份数据库中的所有表2、备份booksDB数据库中的books表3、使用mysqldump备份booksDB和test数据库4、使用mysqldump备份服务器中的所有数据库5、使用mysql命令还原第二题导出的book表6、进入数据库使…

STM32案例学习 GY-39环境监测传感器模块

STM32案例学习 GY-39环境监测传感器模块 硬件平台 野火STM32F1系列开发板正点STM32F1系列开发板STM32F103ZET6核心板GY-39环境监测传感器模块 GY-39环境监测传感器模块 GY-39 是一款低成本&#xff0c;气压&#xff0c;温湿度&#xff0c;光强度传感器模块。工作电压 3-5v…

thinkphp 上传图片

public function upload_img(){// 读取图片资源// 存储路径$path "uploads/avatar";$file request()->file(background_img);// 存储图片$info $file->rule(uniqid)->move($path);// 存储成功if ($info) {//获取到上传图片的路径名称$name_img $path . …

linux查看ipynb文件

linux查看ipynb文件 使用jupyter查看 使用jupyter查看 安装 pip install jupyter添加配置好的环境到jupyter notebook的kernel中&#xff1a; python -m ipykernel install --user --name mmdet --display-name "mmdet"运行jupyter notebook &#xff08;在ipynb…

WebSocket理论和实战

一 WebSocket理论 1.1 什么是http请求 http链接分为短链接、长链接&#xff0c;短链接是每次请求都要三次握手才能发送自己的信息。即每一个request对应一个response。长链接是在一定的期限内保持链接&#xff08;但是是单向的&#xff0c;只能从客户端向服务端发消息&#x…

pycharm import的类库修改后要重启问题的解决方法

通过将以下行添加到pycharm中的settings-> Build,Excecution,Deployment-> Console-> Python Console中&#xff0c;可以指示Pycharm在更改时自动重新加载模块&#xff1a; %load_ext autoreload %autoreload 2

ubuntu 设置系统时间矫正

1、安装ntpdate&#xff0c;同步标准时间 2、修改时区 3、在.profile文件中写入上面提示的信息&#xff0c;保存退出、更新配置文件或者重启生效 3.1、或者配合上面的cp那条命令&#xff0c;用下面的命令保存到底层 $ hwclock --systohc 4、重启之后&#xff0c;查看日期时间已…

中间件上云部署 rocketmq

中间件上云部署 rocketmq rocketmq部署一、rokectmq介绍二、rokectmq特性三、使用rocketmq理由四、rocketmq 核心概念五、rocketmq角色六、rocketmq集群部署方式七、rocketmq集群部署7.1 环境说明7.2 构建rocketmq镜像7.3 获取rocketmq-dashboard镜像7.4 rocketmq部署描述文件编…